Documentation and tooling for the encryption used on player-to-player WiiConnect24 mail
attachments in Animal Crossing: City Folk / Let's Go to the City (RUU*).
This is not the DLC distribution format. Server-sent DLC (RVFOREST / rvforestdl)
is a U8 archive signed with an RSA key and encrypted with a fixed AES key — a completely
separate mechanism handled by tools like ACWC24/ACDLC. What is described here is the
attachment blob that one player's console produces and another player's console consumes
when a letter with an attached item, pattern or NPC travels over WC24 mail.
The attachment is AES-128-CBC, but there is no fixed key in the game. The key and
IV are generated randomly per message and appended to the ciphertext in the clear. The
only thing binding the blob to a sender is the sender's 16-digit Wii Number, which is
mixed into both the AES key and the HMAC key — and which you already have, because it is
the WC24 mail's From: address (w<16 digits>@wii.com).
So "cracking" it needs no secret. You need the envelope.
N is the ciphertext length, always a multiple of 16. Total file size is N + 0x14.
| Offset | Size | Contents |
|---|---|---|
0x00 |
N |
AES-128-CBC ciphertext |
N |
0x10 |
IV — 16 random bytes, stored in the clear |
N + 0x10 |
0x04 |
R — 4 random bytes, stored in the clear |
key = R (4 bytes) || u64be(sender_wii_number) (8 bytes) || 71 71 71 71
iv = the 16 bytes stored at offset N
R is four bytes of RNG output stored raw. The trailing 71 71 71 71 (ASCII qqqq) is a
hardcoded constant. The middle eight bytes are the sender's NWC24 Wii Number as a
big-endian u64 — the same value that appears as the 16-digit decimal From: address.
(The in-game default before a Wii Number is provisioned is 9999999999999999, i.e.
00 23 86 F2 6F C0 FF FF.)
0x00 u64be sender Wii Number (must match the envelope, or the game rejects it)
0x08 char[8] "BU2mori\0" magic
0x10 u32 payload length
0x14 u32 version always 2
0x18 u16 (blob_count << 14) | offset_of_second_payload
0x1A u16 stationery / letter type
0x1C u32 padding
0x20 ... payload[length]
... zero padding to N - 0x18
N-0x18 u32 S — HMAC salt, 4 random bytes
N-0x14 u8[20] HMAC-SHA1
The digest covers plaintext[0 : N-0x18] — i.e. everything before the salt — using a
20-byte HMAC key:
hmac_key = u64be(sender_wii_number) || S || 45 30 39 CB FE 19 DA 72
N is derived from the payload length as N = (length + 0x47) & ~0xF, which is
align16(0x20 header + length + 0x18 trailer).
blob_count in the field at 0x18 is 1 or 2. When it is 2, the low 14 bits give the
offset within the payload at which the second blob starts (item vs. pattern, for
example); when it is 1 the payload is a single blob and the offset is 0.
The u16 at 0x1A lands in the same numbering space that the DLC tooling uses for
stationery, where a paper id is index + 400 over the standard 69-entry paper table
(butterfly = 400 … bulletin = 468). A value of 424 therefore corresponds to Nook
stationery. This mapping is consistent but has not been exhaustively confirmed against
every attachment kind, so treat it as strongly suspected rather than proven.
The decrypted payload itself (town/player records with UTF-16BE names and u16 ids,
the letter header/body/footer, then a short trailer) is only partially mapped and is not
documented here.
Reverse engineered from the USA main.dol (RUUE01). Useful anchors:
| Address | Role |
|---|---|
0x800EA2C8 |
pack + encrypt an attachment |
0x800EA584 |
decrypt + verify an attachment |
0x800EA0DC |
mailbox scan — sender app id RUU\0, attachment type 0x30000, 0x25800 byte cap |
0x80414948 |
NETAESCreateEx(ctx, key, keylen, iv) |
0x80414954 |
AES-CBC encrypt |
0x80414A0C |
AES-CBC decrypt |
0x80413D44 |
NETHMACInit(ctx, alg, key, keylen) |
0x80497110 |
hash descriptor {digest 0x10, block 0x40, …} — MD5 |
0x80497130 |
hash descriptor {digest 0x14, block 0x40, …} — SHA-1, used here |
0x80497150 |
AES S-box (byte-oriented software AES, no T-tables) |
0x80750930 |
the "BU2mori\0" magic, at r2 - 0x7410 |
r13 (SDA) is 0x807516C0 and r2 (SDA2) is 0x80757D40, if you need to resolve other
small-data references while poking at this.
Only three constants are actually hardcoded: 71 71 71 71 (AES key tail),
45 30 39 CB FE 19 DA 72 (HMAC key tail), and the BU2mori magic.
pip install pycryptodome
python3 accf_attach.py <attachment.bin> <sender_wii_number>
Writes <attachment.bin>.dec containing the payload, and prints the parsed header. The
HMAC is verified, so a wrong Wii Number fails loudly rather than producing garbage.
accf_attach.py also exposes encrypt(payload, wii_number, typ, second_offset, count)
for producing blobs the game will accept.
Validated against a real attachment captured from a console: HMAC-SHA1 verified and the
payload decoded cleanly. Round-trips through encrypt → decrypt as well.
General questions or comments can be sent to quatricsoftware@gmail.com. No support will be provided for this tool.
The stationery id mapping (paper id = index + 400) comes from Aurum's ACWC24, which
covers the unrelated server-side DLC distribution format for the same game.
MIT — see LICENSE.
Copyright (c) 2026 quatric