Improve key store passphrase crypto

* Change MAC-then-Encrypt to Encrypt-then-MAC
* Change AES256 to AES128
* Use first 16 bytes of KDF derived key for AES and
  remaining 16 for MAC
This commit is contained in:
Gustav Simonsson 2015-04-02 18:15:58 +02:00
parent 9918b6c84e
commit 6b23094cff
4 changed files with 33 additions and 25 deletions

View File

@ -252,7 +252,7 @@ func aesCBCDecrypt(key []byte, cipherText []byte, iv []byte) (plainText []byte,
decrypter.CryptBlocks(paddedPlainText, cipherText) decrypter.CryptBlocks(paddedPlainText, cipherText)
plainText = PKCS7Unpad(paddedPlainText) plainText = PKCS7Unpad(paddedPlainText)
if plainText == nil { if plainText == nil {
err = errors.New("Decryption failed: PKCS7Unpad failed after decryption") err = errors.New("Decryption failed: PKCS7Unpad failed after AES decryption")
} }
return plainText, err return plainText, err
} }

View File

@ -48,6 +48,7 @@ type plainKeyJSON struct {
} }
type cipherJSON struct { type cipherJSON struct {
MAC []byte
Salt []byte Salt []byte
IV []byte IV []byte
CipherText []byte CipherText []byte

View File

@ -28,24 +28,25 @@ the private key is encrypted and on disk uses another JSON encoding.
Cryptography: Cryptography:
1. Encryption key is scrypt derived key from user passphrase. Scrypt parameters 1. Encryption key is first 16 bytes of SHA3-256 of first 16 bytes of
scrypt derived key from user passphrase. Scrypt parameters
(work factors) [1][2] are defined as constants below. (work factors) [1][2] are defined as constants below.
2. Scrypt salt is 32 random bytes from CSPRNG. It is appended to ciphertext. 2. Scrypt salt is 32 random bytes from CSPRNG.
3. Checksum is SHA3 of the private key bytes. It's stored in plain next to ciphertext in key file.
4. Plaintext is concatenation of private key bytes and checksum. 3. MAC is SHA3-256 of concatenation of ciphertext and last 16 bytes of scrypt derived key.
5. Encryption algo is AES 256 CBC [3][4] 4. Plaintext is the EC private key bytes.
6. CBC IV is 16 random bytes from CSPRNG. It is appended to ciphertext. 5. Encryption algo is AES 128 CBC [3][4]
6. CBC IV is 16 random bytes from CSPRNG.
It's stored in plain next to ciphertext in key file.
7. Plaintext padding is PKCS #7 [5][6] 7. Plaintext padding is PKCS #7 [5][6]
Encoding: Encoding:
1. On disk, ciphertext, salt and IV are encoded in a nested JSON object. 1. On disk, the ciphertext, MAC, salt and IV are encoded in a nested JSON object.
cat a key file to see the structure. cat a key file to see the structure.
2. byte arrays are base64 JSON strings. 2. byte arrays are base64 JSON strings.
3. The EC private key bytes are in uncompressed form [7]. 3. The EC private key bytes are in uncompressed form [7].
They are a big-endian byte slice of the absolute value of D [8][9]. They are a big-endian byte slice of the absolute value of D [8][9].
4. The checksum is the last 32 bytes of the plaintext byte array and the
private key is the preceeding bytes.
References: References:
@ -124,21 +125,25 @@ func (ks keyStorePassphrase) StoreKey(key *Key, auth string) (err error) {
return err return err
} }
keyBytes := FromECDSA(key.PrivateKey) encryptKey := Sha3(derivedKey[:16])[:16]
keyBytesHash := Sha3(keyBytes)
toEncrypt := PKCS7Pad(append(keyBytes, keyBytesHash...))
AES256Block, err := aes.NewCipher(derivedKey) keyBytes := FromECDSA(key.PrivateKey)
toEncrypt := PKCS7Pad(keyBytes)
AES128Block, err := aes.NewCipher(encryptKey)
if err != nil { if err != nil {
return err return err
} }
iv := randentropy.GetEntropyCSPRNG(aes.BlockSize) // 16 iv := randentropy.GetEntropyCSPRNG(aes.BlockSize) // 16
AES256CBCEncrypter := cipher.NewCBCEncrypter(AES256Block, iv) AES128CBCEncrypter := cipher.NewCBCEncrypter(AES128Block, iv)
cipherText := make([]byte, len(toEncrypt)) cipherText := make([]byte, len(toEncrypt))
AES256CBCEncrypter.CryptBlocks(cipherText, toEncrypt) AES128CBCEncrypter.CryptBlocks(cipherText, toEncrypt)
mac := Sha3(derivedKey[16:32], cipherText)
cipherStruct := cipherJSON{ cipherStruct := cipherJSON{
mac,
salt, salt,
iv, iv,
cipherText, cipherText,
@ -177,6 +182,7 @@ func DecryptKey(ks keyStorePassphrase, keyAddr []byte, auth string) (keyBytes []
err = json.Unmarshal(fileContent, keyProtected) err = json.Unmarshal(fileContent, keyProtected)
keyId = keyProtected.Id keyId = keyProtected.Id
mac := keyProtected.Crypto.MAC
salt := keyProtected.Crypto.Salt salt := keyProtected.Crypto.Salt
iv := keyProtected.Crypto.IV iv := keyProtected.Crypto.IV
cipherText := keyProtected.Crypto.CipherText cipherText := keyProtected.Crypto.CipherText
@ -186,15 +192,16 @@ func DecryptKey(ks keyStorePassphrase, keyAddr []byte, auth string) (keyBytes []
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
plainText, err := aesCBCDecrypt(derivedKey, cipherText, iv)
calculatedMAC := Sha3(derivedKey[16:32], cipherText)
if !bytes.Equal(calculatedMAC, mac) {
err = errors.New("Decryption failed: MAC mismatch")
return nil, nil, err
}
plainText, err := aesCBCDecrypt(Sha3(derivedKey[:16])[:16], cipherText, iv)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
keyBytes = plainText[:len(plainText)-32] return plainText, keyId, err
keyBytesHash := plainText[len(plainText)-32:]
if !bytes.Equal(Sha3(keyBytes), keyBytesHash) {
err = errors.New("Decryption failed: checksum mismatch")
return nil, nil, err
}
return keyBytes, keyId, err
} }

View File

@ -1,8 +1,8 @@
package crypto package crypto
import ( import (
"github.com/ethereum/go-ethereum/crypto/randentropy"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto/randentropy"
"reflect" "reflect"
"testing" "testing"
) )