Number: SLIP-0015
Title: Format for Bitcoin metadata and its encryption in HD wallets
Type: Standard
Status: Final
Authors: Karel Bilek <kb@karelbilek.com>
Created: 2015-01-12
SLIP-0015 describes a format to save Bitcoin transaction metadata (labels to accounts, transactions) in a secure way, with regard to HD wallets, especially (but not limited to) hardware HD wallets.
In myTREZOR web wallet, we need to save additional metadata, such as account labels or transaction labels. We had several goals:
- data should be safely saved on a untrustworthy cloud service (such as Dropbox)
- usage should be effortless with secure hardware wallet
- we should allow other applications to use the files, even when they don't support hardware wallets in general
Because we want effortless usage, we want users to be able to add metadata even when they don't have the device connected, or even when they don't actually own the device.
For this reason, we don't want to sign the changes on the secure device and we want to encrypt everything on an unsecure device, with the key in memory. This has the unfortunate consequence of attacker being able to both read and edit metadata if he attacks the unsecure device.
However, we want at least prevent the cloud storage operator to be able to read the metadata. We want to hide the metadata itself from the cloud storage operator, and even the XPUBs of the accounts for deniability.
We first derive a master key from hardware device itself, which is shared for all accounts on the device.
We then derive account key for every account. This key is a string -- because of the stated goal 3., we want to be able to import it into third party applications without HD wallets.
From the account key, we derive both a filename and a symmetric encryption key. We then save the metadata to the given file, in an encrypted JSON.
We first get the master key by sending CipherKeyValue to hardware device with following parameters
- path:
m/10015'/0'
(hardened path, see BIP32) - key:
Enable labeling?
- value:
fedcba98765432100123456789abcdeffedcba98765432100123456789abcdef
(byte sequence, here in hexadecimal) - encrypt: true
- ask_on_encrypt, ask_on_decrypt: true
- iv: unset
CipherKeyValue is defined in SLIP-0011.
The master key should be 32 bytes (256 bits) long. It is treated as a pseudo-random byte sequence.
From the master key, we derive the account key for every account in the following way:
First, we use the HMAC function:
HMAC-SHA256(master key, xpub)
where
- master key is a byte sequence, as defined in the previous section
- xpub is a string, as defined in BIP32. For example:
xpub6BiVtCpG9fQPxnPmHXG8PhtzQdWC2Su4qWu6XW9tpWFYhxydCLJGrWBJZ5H6qTAHdPQ7pQhtpjiYZVZARo14qHiay2fvrX996oEP42u8wZy
Then, the result is converted to string using Base58Check encoding, as used in Bitcoin.
The API key is either 49 or 50 characters long.
We take the account key, as a string, and we use HMAC function to derive filename and password for metadata file. Every account has its own metadata file.
-
First, we use the HMAC function
HMAC-SHA512(API key, constant)
, where-
API key is a string (in base58c) from the previous section.
The API key is taken as a string, so third-party applications can use their own API keys.
-
constant is
0123456789abcdeffedcba9876543210
(byte sequence, here in hexadecimal).
-
-
The result is 64 bytes/512 bits.
-
The first half is used to derive the filename.
The bytes are converted to hexadecimal, which is the used as a filename, with the extension ".mtdt".
We are using hexadecimal instead of base64/base58 because of the ambiguity on case-insensitive filesystems.
-
The second half is used as a key for further encryption, as a byte sequence.
-
-
We are using
AES-256-GCM
algorithm for encryption.- Random 12 bytes are generated as a IV
- GCM is used with the full 128-bit tag
-
The resulting file looks like this:
- first 12 bytes of the file are the random IV
- the next 16 bytes are the GCM authentication tag
- the rest is the ciphertext
The (decrypted) metadata are in following format:
The file is a serialized JSON object with the following keys:
version
: version of metadata format, for future backwards compatibility. The version is currently1.0.0
.accountLabel
: label for the account, a stringoutputLabels
: labels for outputs, described furtheraddressLabels
: labels for addresses, described further
outputLabels
has transaction hashes for keys, and for values it has object with output indexes for keys and output labels, as strings, for values. Output indexes start at 0.
addressLabels
has addresses (in traditional Base58Check encoding) for keys and their labels for values. Only receiving addresses are saved in this object.
All labels can have any unicode letters. Empty string is treated in the software as having no label.
An example object looks like this:
{
"version": "1.0.0",
"accountLabel": "Saving account", // one file per account, so only 1 label needed
"addressLabels": {
"1JAd7XCBzGudGpJQSDSfpmJhiygtLQWaGL": "My receiving address",
"1GWFxtwWmNVqotUPXLcKVL2mUKpshuJYo": "" // equivalent to no label set or null
},
"outputLabels": {
"350eebc1012ce2339b71b5fca317a0d174abc3a633684bc65a71845deb596539": {
"0": "Money to Adam",
"1": "" // equivalent to no label set
},
"ebbd138134e2c8acfee4fd4edb6f7f9175ee7b4020bcc82aba9a13ce06fae85b": {
"0": "Feeding bitcoin eater"
}
}
}
(comments are of course not part of a valid JSON and are included here only for clarity)
All the example code is in Python2.
Example code, deriving a master key from a connected TREZOR is in 1_masterkey.py. It requires python-trezor installed and TREZOR connencted
For the "stress test" wallet, defined in SLIP-0014, the master key should be (in hex):
20c8bf0701213cdcf4c2f56fd0096c1772322d42fb9c4d0ddf6bb122d713d2f3
Example code, deriving an account key for master key, is in 2_accountkey.py. First argument of the script is xpub of the account, the second argument is the master key from previous step (in hexadecimal).
For the "stress test" wallet, defined in SLIP-0014, and its first account (with the xpub xpub6BiVtCp...
), the key should be:
v5kCxSKLTsnwmgPBeaRyFDWeG9zXouF34L72763zjLrS4LWy8
Example code for decryption is in 3_decrypt.py. First and only argument is the account key from previous step. The file has to be in a current working directory (in myTREZOR, we use ~/Dropbox/Apps/TREZOR/
for saving the files).
With the key v5kCxSKLTsnwmgPBeaRyFDWeG9zXouF34L72763zjLrS4LWy8
, filename 08108c3a46882bb71a5df59f4962e02f89a63efb1cf5f32ded94694528be6cec.mtdt
and the data (in hex)
d32a5831b74ba04cdf44309fbb96a1b464fe5d4a27d1e753c30602ba1947
3cca7d8734e8b9442dbd41d530c42e03fea59a5d38b21392f3e4a135eb07
009d5a8b9996055b7aff076918c4ed63ee49db56c5a6b069cac7f221f704
5af7197cdbb562ba004d7a6f06eb7cffd1dfb177fd652e66c2d05d944b58
85d6a104853a0d07e4cebff3513a2f6a1c8ff6f4f98ce222f3d601f1c796
d070b7523649e10242dfe78cb2db50e826dd18b1f65213f5c0748577ecc9
7b8e13ab9cd0c5fe7b76635717c64ad352064a3321df6bbfa2db8ef8c692
55ef9d8a8dfbce9c6ad3029bbdcf1b2bb04795fd96aa95d27e6ca1ed2658
bfb108b44dac2159184d6e3cabe341e2ec5d83756aeb8c408e92fe6ca3e6
3d4c0d644aa2648341506324574d205934c65f54979b1d684f7a2442e8d5
2149ed67449019e6091aa182afcaf5aa1fa8bf3114ee7b46e47b4c6648d1
d1355cefd10081be6e8c7bdf1b2ff14d8896b1ede811fa1aa2c024a6ebf3
6baf0a8d6afa2975bf551e8bc3f03117b42dc4cbe2a6bd700f2fda40c78a
48627ebc130286ba98
we should get to file, similar to the one described above.
Similarly, in 4_encrypt.py there is an example code for encrypting.