> For the complete documentation index, see [llms.txt](https://bsv.brc.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://bsv.brc.dev/outpoints/0039.md).

# User Wallet Data Format Encryption Extension

Ty Everett (<ty@projectbabbage.com>)

## Abstract

This specification defines the canonical encrypted wrapper for [BRC-38](/outpoints/0038.md) 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](/wallet/0002.md)
* a portable, versioned binary envelope in the spirit of [BRC-78](/peer-to-peer/0078.md)
* 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:

1. serialize a valid BRC-38 payload
2. encrypt that payload into a valid BRC-39 file
3. decrypt and validate a valid BRC-39 file
4. 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:

1. interpreted as a Unicode string
2. normalized to NFC
3. 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:

* `0x02` to `0xff`

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 Format` MUST be `38`

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:

* `Flags` MUST be `0x00`

#### 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:

1. construct a valid BRC-38 document
2. serialize it as canonical UTF-8 JSON
3. normalize the user password to NFC and encode it as UTF-8
4. generate a random 32-byte salt
5. derive a 32-byte key using Argon2id and the encoded password
6. generate a random 32-byte nonce
7. construct the BRC-39 header
8. encrypt the BRC-38 plaintext using AES-256-GCM with:
   * key = derived key
   * nonce = header nonce
9. append `CiphertextAndTag`
10. emit the resulting bytes as `wallet.brc39`

### 8. Import Procedure

To import a BRC-39 file, an importer MUST:

1. parse and validate the binary header
2. 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
3. obtain the user password
4. normalize the password to NFC and encode it as UTF-8
5. derive the decryption key using the encoded KDF parameters from the file
6. decrypt `CiphertextAndTag` with AES-256-GCM using:
   * key = derived key
   * nonce = header nonce
7. reject the file if GCM authentication fails
8. parse the plaintext as UTF-8 JSON
9. 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:

* `Magic` is not `WDAT`
* `Format Version` is unsupported
* `Protector Type` is unsupported
* `Inner Format` is not `38`
* `KDF Type` is not `0x01`
* `Flags` is not `0x00`
* `Salt Length` is less than 8 (the Argon2 reference implementation refuses a shorter salt)
* `Nonce Length` is 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:

```json
{"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:

1. exporting a full BRC-38 payload for the selected user
2. deriving a password key with Argon2id using the standardized Toolbox-compatible default parameters
3. encrypting the payload with AES-256-GCM
4. writing the standardized binary envelope described above

## References

* [BRC-2: Data Encryption and Decryption](/wallet/0002.md)
* [BRC-38: User Wallet Data Format](/outpoints/0038.md)
* [BRC-40: User Wallet Data Synchronization](/outpoints/0040.md)
* [BRC-78: Serialization Format for Portable Encrypted Messages](/peer-to-peer/0078.md)
* [Argon2](https://www.rfc-editor.org/rfc/rfc9106)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://bsv.brc.dev/outpoints/0039.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
