Abstract
This specification defines the canonical encrypted wrapper for BRC-38 user wallet data exports.
It standardizes a downloadable binary file format, conventionally named wallet.brc39, for password-protected wallet export and import. The design is intended for strong user data portability, backups, archival export, GDPR-style data access, and migration between wallet implementations and storage providers.
BRC-39 uses:
- the BRC-38 payload as its canonical plaintext inner document
- the AES-256-GCM symmetric encryption primitive used by BRC-2
- a portable, versioned binary envelope in the spirit of BRC-78
- Argon2id as the mandatory password key-derivation function for new exports
Motivation
BRC-38 defines what wallet data must be portable. It does not define how that data should be safely delivered to a user as a single downloadable file.
For real-world portability, the default export artifact must be:
- encrypted
- password protected
- self-describing
- suitable for offline storage
- stable enough to move between implementations
This document defines that artifact.
Specification
1. Scope
This specification defines:
- the canonical encrypted file format for a single-user BRC-38 export
- password normalization rules
- key derivation rules
- authenticated encryption rules
- file parsing, validation, export, and import behavior
It does not define:
- secret-sharing or recovery-key workflows
- multi-file backup sets
- multi-user containers
- wallet-runtime key snapshots unrelated to BRC-38
2. Conformance
An implementation conforms to BRC-39 if it can:
- serialize a valid BRC-38 payload
- encrypt that payload into a valid BRC-39 file
- decrypt and validate a valid BRC-39 file
- recover the original BRC-38 payload without loss
3. Canonical Export Artifact
The canonical encrypted export artifact:
- MUST be a binary file
- SHOULD be named
wallet.brc39 - SHOULD use media type
application/vnd.brc39.wallet
The inner plaintext payload MUST be a complete BRC-38 document serialized as canonical UTF-8 JSON.
4. Cryptographic Profile
4.1 Plaintext
The plaintext to be encrypted is:
- the exact UTF-8 byte sequence of a valid canonical BRC-38 JSON document
4.2 Password Encoding
Before key derivation, the user password MUST be:
- interpreted as a Unicode string
- normalized to NFC
- encoded as UTF-8 bytes
No additional trimming, case folding, or whitespace normalization is permitted.
4.3 Key Derivation
New BRC-39 exports MUST use Argon2id.
The canonical derived-key length is:
- 32 bytes
The canonical default Argon2id parameters are:
- iterations:
7 - memoryKiB:
131072 - parallelism:
1 - hashLength:
32 - saltLength:
32
These defaults align with the Wallet Toolbox Argon2id defaults this specification is intended to standardize.
Implementations MAY use stronger Argon2id parameters within the interoperability bounds of Section 11.1, and MUST encode the exact parameters used in the file header.
4.4 Authenticated Encryption
The content-encryption algorithm is AES-256-GCM.
The content-encryption key is the 32-byte Argon2id-derived key.
The file nonce length is:
- 32 bytes
The nonce MUST be generated randomly for each export.
The GCM authentication tag length MUST be 16 bytes.
4.5 Header Validation
Header fields are validated before decryption as specified below. BRC-39 encrypts only the BRC-38 plaintext payload.
5. File Layout
All integers in the binary file MUST be unsigned and encoded in big-endian byte order.
The canonical BRC-39 file layout is:
| Field | Length | Description |
|---|---|---|
| Magic | 4 bytes | ASCII WDAT |
| Format Version | 1 byte | Currently 0x01 |
| Protector Type | 1 byte | Currently 0x01 for password-protected export |
| Inner Format | 1 byte | Currently 0x26 (decimal 38) for BRC-38 |
| KDF Type | 1 byte | Currently 0x01 = Argon2id |
| Flags | 1 byte | Currently 0x00; all bits reserved and MUST be zero |
| Salt Length | 1 byte | Currently 0x20 (32) |
| Nonce Length | 1 byte | Currently 0x20 (32) |
| Argon2id Iterations | 4 bytes | Work factor |
| Argon2id Memory KiB | 4 bytes | Memory cost |
| Argon2id Parallelism | 1 byte | Parallelism |
| Argon2id Hash Length | 1 byte | Derived key length; currently 32 |
| Reserved | 12 bytes | MUST be all zero |
| Salt | variable | Salt Length bytes |
| Nonce | variable | Nonce Length bytes |
| CiphertextAndTag | variable | AES-256-GCM ciphertext followed by 16-byte tag |
6. Header Field Requirements
6.1 Magic
The file MUST begin with the ASCII bytes:
57 44 41 54
which spell WDAT.
6.2 Format Version
The current format version is 1.
Files with unsupported versions MUST be rejected.
6.3 Protector Type
The following protector types are defined:
0x01: password-based encryption
The following values are reserved for future extension:
0x02to0xff
This version of BRC-39 standardizes only password-based encryption.
6.4 Inner Format
The Inner Format field identifies the plaintext payload.
For BRC-39 wallet exports:
Inner FormatMUST be38
Meaning the decrypted plaintext MUST be a valid BRC-38 document.
6.5 KDF Type
The following KDF type is defined:
0x01: Argon2id
All other KDF type values are reserved for future specifications. This version of BRC-39 defines no alternate KDF import mode.
6.6 Flags
All flag bits are reserved for future specifications.
For this specification:
FlagsMUST be0x00
6.7 Reserved Bytes
All reserved bytes MUST be zero on export and MUST be ignored only if zero on import. Non-zero reserved bytes MUST cause rejection.
7. Export Procedure
To create a BRC-39 file, an exporter MUST:
- construct a valid BRC-38 document
- serialize it as canonical UTF-8 JSON
- normalize the user password to NFC and encode it as UTF-8
- generate a random 32-byte salt
- derive a 32-byte key using Argon2id and the encoded password
- generate a random 32-byte nonce
- construct the BRC-39 header
- encrypt the BRC-38 plaintext using AES-256-GCM with:
- key = derived key
- nonce = header nonce
- append
CiphertextAndTag - emit the resulting bytes as
wallet.brc39
8. Import Procedure
To import a BRC-39 file, an importer MUST:
- parse and validate the binary header
- verify
Magic,Format Version,Protector Type,Inner Format,KDF Type,Flags, reserved bytes,Salt Length,Nonce Length, and the Argon2id parameters, as Sections 11 and 11.1 require - obtain the user password
- normalize the password to NFC and encode it as UTF-8
- derive the decryption key using the encoded KDF parameters from the file
- decrypt
CiphertextAndTagwith AES-256-GCM using:- key = derived key
- nonce = header nonce
- reject the file if GCM authentication fails
- parse the plaintext as UTF-8 JSON
- validate that the plaintext is a valid BRC-38 document
Only after successful BRC-38 validation may the importer proceed to import wallet data into local storage.
9. KDF Compatibility
BRC-39 standardizes only Argon2id for password-based wallet export encryption.
Implementations MUST reject files whose KDF Type is not 0x01. Compatibility with pre-standard password-derived wallet internals is outside the BRC-39 file format.
10. Relationship to BRC-2 and BRC-78
10.1 BRC-2
BRC-39 uses the same symmetric encryption primitive family as BRC-2:
- AES-256-GCM
Unlike BRC-2 application messages, BRC-39 stores the nonce explicitly in the binary file header instead of prepending it to a message payload.
10.2 BRC-78
BRC-39 follows the same general design philosophy as BRC-78:
- binary, versioned, self-describing encrypted envelopes
BRC-78 is oriented toward portable encrypted inter-party messages using BRC-2/BRC-42/BRC-43 derivation. BRC-39 instead targets password-protected at-rest wallet export. This version does not standardize recipient-encrypted wallet exports, but future protector types MAY define such a mode.
11. Validation Rules
A BRC-39 file MUST be rejected if:
Magicis notWDATFormat Versionis unsupportedProtector Typeis unsupportedInner Formatis not38KDF Typeis not0x01Flagsis not0x00Salt Lengthis less than 8 (the Argon2 reference implementation refuses a shorter salt)Nonce Lengthis zero- reserved bytes are non-zero
- any Argon2id parameter is invalid, including:
- iterations is zero
- memoryKiB is zero
- parallelism is zero
- memoryKiB is less than 8 times parallelism (RFC 9106, Section 3.1)
- hashLength is not
32
- any Argon2id parameter is outside the interoperability bounds of Section 11.1
- AES-GCM authentication fails
- the decrypted payload is not valid UTF-8 JSON
- the decrypted payload is not a valid BRC-38 document
11.1 Resource Limits
An importer has to derive the key before it can check the authentication tag, so the Argon2id parameters in the header decide how much memory and time it spends on a file before it can tell whether the file is genuine. Without a limit, a file of under a hundred bytes can ask for almost 4 TiB of memory or more than four billion passes, and a file written with stronger parameters than another importer supports does not open there.
The interoperability bounds are: iterations at most 14, memoryKiB at most 262144 (256 MiB), and parallelism at most 4. The canonical defaults of Section 4.3 are within them.
- An exporter MUST NOT write parameters outside the interoperability bounds.
- An importer MUST refuse a file whose parameters are outside the interoperability bounds, before deriving any key.
- An importer MUST NOT refuse a file because of parameters within the interoperability bounds.
12. Privacy and Operational Requirements
BRC-39 files contain the complete user wallet export, protected only by the chosen password and KDF parameters.
Wallet implementations:
- MUST warn users that weak passwords materially weaken the file
- SHOULD encourage long passwords or passphrases
- SHOULD avoid embedding password hints in the file
- SHOULD default to Argon2id parameters at least as strong as the canonical defaults
13. Canonical Example Header Semantics
An example canonical new-export header therefore means:
- file type:
wallet.brc39 - payload type: BRC-38
- protection mode: password
- KDF: Argon2id
- salt: 32 random bytes
- nonce: 32 random bytes
- plaintext: canonical UTF-8 BRC-38 JSON
14. Test Vectors
All byte strings are hexadecimal. The plaintext of both positive vectors is this canonical BRC-38 document (874 bytes of UTF-8, SHA-256 e76c1eb7a57cdf87e8d8a9c50b783e50decffb54c586bb86fa63186d7a31e48c), a synthetic export with placeholder keys:
{"brc":38,"exportedAt":"2026-01-02T03:04:05.006Z","formatVersion":1,"sourceStorage":{"chain":"test","created_at":"2026-01-02T03:04:05.006Z","dbtype":"SQLite","maxOutputScript":1024,"storageIdentityKey":"021111111111111111111111111111111111111111111111111111111111111111","storageName":"synthetic","updated_at":"2026-01-02T03:04:05.006Z"},"tables":{"certificateFields":[],"certificates":[],"commissions":[],"outputBaskets":[],"outputTagMaps":[],"outputTags":[],"outputs":[],"provenTxReqs":[],"provenTxs":[],"syncStates":[],"transactions":[],"txLabelMaps":[],"txLabels":[]},"title":"User Wallet Data Format","user":{"activeStorage":"021111111111111111111111111111111111111111111111111111111111111111","created_at":"2026-01-02T03:04:05.006Z","identityKey":"032222222222222222222222222222222222222222222222222222222222222222","updated_at":"2026-01-02T03:04:05.006Z","userId":1}}
14.1 Vector 1: canonical parameters, fixed salt and nonce
| Item | Value |
|---|---|
| Password | Café wallet export |
| Password, NFC, UTF-8 | 436166c3a92077616c6c6574206578706f7274 |
| Password, NFD, UTF-8 | 43616665cc812077616c6c6574206578706f7274, which normalizes to the NFC bytes and so derives the same key |
| Salt (32 bytes) | 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f |
| Nonce (32 bytes) | 202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f |
| Argon2id | iterations 7, memoryKiB 131072, parallelism 1, hashLength 32 |
| Derived key | bf0d243b3ac3d65758c7b9e133431947463df931b3574a2dc8eb3ea70e42bc55 |
| Header (33 bytes) | 574441540101260100202000000007000200000120000000000000000000000000 |
| First 16 bytes of ciphertext | dc61aed5af05d98600370dfdb1bffeb3 |
| Authentication tag | 7d5ad6d1fe8723ed510de2dc84f69a57 |
| File | 987 bytes (header, salt, nonce, ciphertext, tag), SHA-256 90e7c4cb45ae17b529374a3b8c86f7147cdbbf5af5efe4fa9b1d5fda8b84a30a |
An exporter given these inputs writes exactly this file, and an importer given either form of the password opens it to the document above.
14.2 Vector 2: a file written by Wallet Toolbox
Wallet Toolbox 2.14.4 wrote this file with its own random salt and nonce, the canonical parameters, and the password Café synthetic pw (UTF-8 436166c3a92073796e746865746963207077). It opens to the same document. 987 bytes, SHA-256 6343ecc88ba3f38228e419fa1803be78a178e13f0f059537b395d9de472f6e6f:
574441540101260100202000000007000200000120000000000000000000000000a1ccb54521ec73a2f4fea5c40fb288
aa614ccdc45e783c5bda609f7c46fb6d6476ce7771b08242281f9da710ecf6d1c8a942c3e754a3d42e8d2082e597768c
5c3fc684dacb78388c339b2621fddd7cbeeb7ad00be25d57cc520ce717f8ea661fdbd29199509f5e625826f03158543d
5fcb7f86eac816e639b51bce84cec760789cba75c3cc6a0b0fd210621f45dca818c39ebdac56f3016c06869ebe08dc0a
2cbdeb39c47139c38a335fff9f9c1c52d19c8a4d5d477a0f9321a4069cd0ca5621d3e14d9653dd5710919ff864806be3
6723fd39ba833137315a41bd87eec1c895d4f805b2f5077ea2598557a0d9c41e68a978ed5263f53efaf0146cca9db5db
79a4e0e14ce6d48581e16623c87000e02b3c59086053ec8544251728138cb6c283f2910945eabbc66a91358f9a4534e8
59c09c04f41e84180580c9555b47e05fde7d3ffe862a3ce558e7dca53e29517387ad7342d4fee7f12553cc5251cfa7ff
e4faabe28234d8bb137dfed71dfbdd413c72aa66df2d7cbb378cb626aeb545ce2f070865d2b6f07784cc70dc9875fbb5
b05bbd6a52f47561c9e83d26180a4a04d127fe732a5343547037be749141dd77dde9b758ab66af54871831078f56e2bb
dce84856a012a8f597afe3a94991ff5ce2c87770773f6438e71fa12995d30dc8b6b5eedbb7c051ac042bf496e4f4bbf7
569cd0ff770aa6f25e84a60f348cfe953c4ddaef48128ec961b288c170a1b23d87f0223f1bbe9997fecf6ea56e7380c2
d337fce2d8ade729170b42473c0d2d61d2e54ca50306b00e3d3480cdb0bda2676c89855c92ecda20e51347108483e74c
b48901074cb36fabf31c34087adce028f640bcf22c9ff0c7d19bc6c27e3d7bbbb99ad1ff963b2d01d86aa6a7fbc504fa
e330abc0753097f401e29a1b430f55508b35a4908ab419d84407eaa1630971a3ab98833732e5f8e624f36ce9f9960aa6
8e765a8d851906c6ff3363f0aa8963f1b96aeae7de34b044098f35a08325d420e071b660bc9defb0b10986477dd13d3f
436c86bf7df5032465a8232047e140dc1c227069217a377cf59ab94f0dbbecaebbd94df6db6efee510d39da425d03f16
0b9fe81699d882ec19ac5ea657e6dcbbc843ec04642c367a3264bf2ed5be606a914c2d99552c42d8672b65d8eca2dd15
d16ccc8963fc48aaf9a4412c88f453ab05adb37ae3b8b5cd23fb1f25106ab5e034859c934958610c13b391ea6c861f0d
bd194ffbe71022cd93742a07827e497dfb2cb7b9126078e73364344b74a11daed477d9a8d1f07b31bd43fe989066e005
69b71afedda4fd5f4e4aee6ebbec64988ea9aeb510c617ea5b26e2
14.3 Files an importer must refuse
Each row edits Vector 1 at the byte offset given (0 is the first byte of the file) and is tried with Vector 1's password. An importer refuses a header that breaks Section 11 or 11.1 before it derives any key, so it never does the work such a header asks for. A valid header with another work factor derives another key, so authentication fails, as it does for an edited ciphertext or tag.
| Edit | Offset | New value | Refused |
|---|---|---|---|
magic WDAX |
3 | 0x58 |
before key derivation |
| format version 2 | 4 | 0x02 |
before key derivation |
| protector type 2 | 5 | 0x02 |
before key derivation |
| inner format 39 | 6 | 0x27 |
before key derivation |
| KDF type 2 | 7 | 0x02 |
before key derivation |
flags 0x01 |
8 | 0x01 |
before key derivation |
| salt length 0 | 9 | 0x00 |
before key derivation |
| nonce length 0 | 10 | 0x00 |
before key derivation |
| iterations 0 | 11-14 | 00000000 |
before key derivation |
| memoryKiB 0 | 15-18 | 00000000 |
before key derivation |
| parallelism 0 | 19 | 0x00 |
before key derivation |
| hash length 31 | 20 | 0x1f |
before key derivation |
a reserved byte 0x01 |
32 | 0x01 |
before key derivation |
| salt length 7 | 9 | 0x07 |
before key derivation |
| memoryKiB 31 with parallelism 4 | 15-19 | 0000001f04 |
before key derivation |
| iterations 15 | 11-14 | 0000000f |
before key derivation (Section 11.1) |
| memoryKiB 262145 | 15-18 | 00040001 |
before key derivation (Section 11.1) |
| parallelism 5 | 19 | 0x05 |
before key derivation (Section 11.1) |
| iterations 4294967295 | 11-14 | ffffffff |
before key derivation (Section 11.1) |
| memoryKiB 4294967295 | 15-18 | ffffffff |
before key derivation (Section 11.1) |
| iterations 8 | 11-14 | 00000008 |
after key derivation: authentication fails |
| the first ciphertext byte | 97 | its value XOR 0x01 |
after key derivation: authentication fails |
| the last tag byte | 986 | its value XOR 0x01 |
after key derivation: authentication fails |
Implementation
An implementation based on Wallet Toolbox can produce a BRC-39 file by:
- exporting a full BRC-38 payload for the selected user
- deriving a password key with Argon2id using the standardized Toolbox-compatible default parameters
- encrypting the payload with AES-256-GCM
- writing the standardized binary envelope described above