Private keys in base-58 export format are great for moving between bitcoin wallets, or for safe keeping outside of wallet.dat. They are currently used by several 3rd party systems, including BitBills, vanitygen, and pywallet. However, they are very vulnerable to being leaked or stolen if certain security measures aren't taken. While the built-in bitcoin wallet.dat encryption is able to encrypt private keys stored in wallet.dat, this protection does not extend to exported keys. The format proposed here is an extension to the base-58 export format with integrated password protection.
This format uses strong encryption and multi-iteration password-based key derivation to encrypt a bitcoin private key. As with any format of this type, the password can be cracked. However, by current standards, it is very difficult, of similar difficulty to cracking passwords for WiFi WPA2-PSK. Complex passwords with no dictionary words of at least nine characters will require more than three years to crack for a very resourceful attacker.
Updated: The current scheme is as follows. Thanks go to pixelglow for smart suggestions.
privkey = 32-byte EC private key, big-endian form
param = Parameter descriptor byte.
- 0: Brief format -- unpadded cipher, 4-byte salt, HMAC-SHA256 password check value
- pbhash = HMAC-SHA256
- pbiter = 4096
- cipher = AES-256-CBC
- 16: PKCS#7-compliant format -- padded cipher, 8-byte salt
- pbhash = HMAC-SHA256
- pbiter = 4096
- cipher = AES-256-CBC
For Brief format:
salt = 4-byte random value
key =
PBKDF2(password, salt, pbhash, iter)(
key is then split into
cipherkeyiv and
hmackey.
hmackey is 16 bytes long.)
pwcheck =
HMAC-SHA256(hmackey, privkey)protkey = param | cipher(privkey, cipherkeyiv, unpadded) | pwcheck[0:64] | saltResult is 45 bytes for most ciphers
For PKCS#7-compliant format:
salt = 8-byte random value
cipherkey =
PBKDF2(password, salt, pbhash, iter)protkey = param | cipher(privkey, cipherkey) | saltResult is 58 bytes for most ciphers
The
protkey value then has a data class byte (32 or 79) prefixed to the beginning, and is base-58 encoded as per the bitcoin function
EncodeBase58Check(). Below is an example of a private key password-protected using this scheme, with a test password
password1:
Address: 1HacknYhLRpttqGAiBew1fQAKRGnMorkxk
Privkey: 5JmsuCoDBDTPT8oBxx1Vv4cy9Fw14456rt7hvMKmvQY1DES2w6z
Protkey: PsQg61gLtNX6bg7PG8Kw9bpdFEfuP8h1Ri8dAoBjRpV1i1rPPx72EYrGgi7CfWWkbutH
Protkey: 2uhT7zkgeGXpVEw4aYTnCyAvTQ9G1hqSGPPNS5EpC4W62J28euHT8o9CQnKrZCqYwhcHgrxBASsWzYT bF3Qcx
(
Careful: the long form above has an extra space added by the forum)
The Brief representation makes some specific compromises to maintain a short representation. In this mode, the block cipher is performed with no PKCS#7 padding at the end. Since the padding provides a way to verify the correctness of the password, the HMAC value
pwcheck is computed and added to the output. The
pwcheck value is computed using the HMAC-SHA256 hash of the plaintext private key, using 16 bytes of key material taken from the PBKDF2 function as the message key. This saves a few bytes in the final representation. The salt value is also only four bytes long, shorter than the recommended eight.
This scheme, unless somebody can poke a big hole in it or recommend a useful change, will show up as an optional output format in the next release of vanitygen, along with a password codec.
A JavaScript example of the password codec is available
here. It does run in your browser but does not transmit keys or passwords over the network.
Bounty: I don't have many bitcoins to throw at this, but 5 BTC go to the first person who can remove the protection from the key below:
1NPHardFPnejsycvne2gvirm5MCr9gnAbf
PsV8XM6n5FvjonHPwwjzqvzA6UXruAAJfQ5VwbXFCJrSQEiQ4gyNNDhuYfL8JUBt6mxtTo sweeten the deal: the password is 14 characters long, with upper and lower case letters only, and contains at least one dictionary word.