// Imported from github.com/ethereum/go-ethereum/accounts/keystore/passphrase.go // and github.com/ethereum/go-ethereum/accounts/keystore/presale.go package keystore import ( "bytes" "crypto/aes" "crypto/cipher" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "github.com/pborman/uuid" "golang.org/x/crypto/pbkdf2" "golang.org/x/crypto/scrypt" "github.com/status-im/status-go/eth-node/crypto" "github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/extkeys" ) const ( keyHeaderKDF = "scrypt" ) type encryptedKeyJSONV3 struct { Address string `json:"address"` Crypto CryptoJSON `json:"crypto"` Id string `json:"id"` Version int `json:"version"` ExtendedKey CryptoJSON `json:"extendedkey"` SubAccountIndex uint32 `json:"subaccountindex"` } type encryptedKeyJSONV1 struct { Address string `json:"address"` Crypto CryptoJSON `json:"crypto"` Id string `json:"id"` Version string `json:"version"` } type CryptoJSON struct { Cipher string `json:"cipher"` CipherText string `json:"ciphertext"` CipherParams cipherparamsJSON `json:"cipherparams"` KDF string `json:"kdf"` KDFParams map[string]interface{} `json:"kdfparams"` MAC string `json:"mac"` } type cipherparamsJSON struct { IV string `json:"iv"` } // DecryptKey decrypts a key from a json blob, returning the private key itself. func DecryptKey(keyjson []byte, auth string) (*types.Key, error) { // Parse the json into a simple map to fetch the key version m := make(map[string]interface{}) if err := json.Unmarshal(keyjson, &m); err != nil { return nil, err } // Depending on the version try to parse one way or another var ( keyBytes, keyId []byte err error extKeyBytes []byte extKey *extkeys.ExtendedKey ) subAccountIndex, ok := m["subaccountindex"].(float64) if !ok { subAccountIndex = 0 } if version, ok := m["version"].(string); ok && version == "1" { k := new(encryptedKeyJSONV1) if err := json.Unmarshal(keyjson, k); err != nil { return nil, err } keyBytes, keyId, err = decryptKeyV1(k, auth) if err != nil { return nil, err } extKey, err = extkeys.NewKeyFromString(extkeys.EmptyExtendedKeyString) } else { k := new(encryptedKeyJSONV3) if err := json.Unmarshal(keyjson, k); err != nil { return nil, err } keyBytes, keyId, err = decryptKeyV3(k, auth) if err != nil { return nil, err } extKeyBytes, err = decryptExtendedKey(k, auth) if err != nil { return nil, err } extKey, err = extkeys.NewKeyFromString(string(extKeyBytes)) } // Handle any decryption errors and return the key if err != nil { return nil, err } key := crypto.ToECDSAUnsafe(keyBytes) return &types.Key{ ID: uuid.UUID(keyId), Address: crypto.PubkeyToAddress(key.PublicKey), PrivateKey: key, ExtendedKey: extKey, SubAccountIndex: uint32(subAccountIndex), }, nil } func DecryptDataV3(cryptoJson CryptoJSON, auth string) ([]byte, error) { if cryptoJson.Cipher != "aes-128-ctr" { return nil, fmt.Errorf("Cipher not supported: %v", cryptoJson.Cipher) } mac, err := hex.DecodeString(cryptoJson.MAC) if err != nil { return nil, err } iv, err := hex.DecodeString(cryptoJson.CipherParams.IV) if err != nil { return nil, err } cipherText, err := hex.DecodeString(cryptoJson.CipherText) if err != nil { return nil, err } derivedKey, err := getKDFKey(cryptoJson, auth) if err != nil { return nil, err } calculatedMAC := crypto.Keccak256(derivedKey[16:32], cipherText) if !bytes.Equal(calculatedMAC, mac) { return nil, ErrDecrypt } plainText, err := aesCTRXOR(derivedKey[:16], cipherText, iv) if err != nil { return nil, err } return plainText, err } func decryptKeyV3(keyProtected *encryptedKeyJSONV3, auth string) (keyBytes []byte, keyId []byte, err error) { if keyProtected.Version != version { return nil, nil, fmt.Errorf("Version not supported: %v", keyProtected.Version) } keyId = uuid.Parse(keyProtected.Id) plainText, err := DecryptDataV3(keyProtected.Crypto, auth) if err != nil { return nil, nil, err } return plainText, keyId, err } func decryptKeyV1(keyProtected *encryptedKeyJSONV1, auth string) (keyBytes []byte, keyId []byte, err error) { keyId = uuid.Parse(keyProtected.Id) mac, err := hex.DecodeString(keyProtected.Crypto.MAC) if err != nil { return nil, nil, err } iv, err := hex.DecodeString(keyProtected.Crypto.CipherParams.IV) if err != nil { return nil, nil, err } cipherText, err := hex.DecodeString(keyProtected.Crypto.CipherText) if err != nil { return nil, nil, err } derivedKey, err := getKDFKey(keyProtected.Crypto, auth) if err != nil { return nil, nil, err } calculatedMAC := crypto.Keccak256(derivedKey[16:32], cipherText) if !bytes.Equal(calculatedMAC, mac) { return nil, nil, ErrDecrypt } plainText, err := aesCBCDecrypt(crypto.Keccak256(derivedKey[:16])[:16], cipherText, iv) if err != nil { return nil, nil, err } return plainText, keyId, err } func decryptExtendedKey(keyProtected *encryptedKeyJSONV3, auth string) (plainText []byte, err error) { if len(keyProtected.ExtendedKey.CipherText) == 0 { return []byte(extkeys.EmptyExtendedKeyString), nil } if keyProtected.Version != version { return nil, fmt.Errorf("Version not supported: %v", keyProtected.Version) } if keyProtected.ExtendedKey.Cipher != "aes-128-ctr" { return nil, fmt.Errorf("Cipher not supported: %v", keyProtected.ExtendedKey.Cipher) } mac, err := hex.DecodeString(keyProtected.ExtendedKey.MAC) if err != nil { return nil, err } iv, err := hex.DecodeString(keyProtected.ExtendedKey.CipherParams.IV) if err != nil { return nil, err } cipherText, err := hex.DecodeString(keyProtected.ExtendedKey.CipherText) if err != nil { return nil, err } derivedKey, err := getKDFKey(keyProtected.ExtendedKey, auth) if err != nil { return nil, err } calculatedMAC := crypto.Keccak256(derivedKey[16:32], cipherText) if !bytes.Equal(calculatedMAC, mac) { return nil, ErrDecrypt } plainText, err = aesCTRXOR(derivedKey[:16], cipherText, iv) if err != nil { return nil, err } return plainText, err } func getKDFKey(cryptoJSON CryptoJSON, auth string) ([]byte, error) { authArray := []byte(auth) salt, err := hex.DecodeString(cryptoJSON.KDFParams["salt"].(string)) if err != nil { return nil, err } dkLen := ensureInt(cryptoJSON.KDFParams["dklen"]) if cryptoJSON.KDF == keyHeaderKDF { n := ensureInt(cryptoJSON.KDFParams["n"]) r := ensureInt(cryptoJSON.KDFParams["r"]) p := ensureInt(cryptoJSON.KDFParams["p"]) return scrypt.Key(authArray, salt, n, r, p, dkLen) } else if cryptoJSON.KDF == "pbkdf2" { c := ensureInt(cryptoJSON.KDFParams["c"]) prf := cryptoJSON.KDFParams["prf"].(string) if prf != "hmac-sha256" { return nil, fmt.Errorf("Unsupported PBKDF2 PRF: %s", prf) } key := pbkdf2.Key(authArray, salt, c, dkLen, sha256.New) return key, nil } return nil, fmt.Errorf("Unsupported KDF: %s", cryptoJSON.KDF) } // TODO: can we do without this when unmarshalling dynamic JSON? // why do integers in KDF params end up as float64 and not int after // unmarshal? func ensureInt(x interface{}) int { res, ok := x.(int) if !ok { res = int(x.(float64)) } return res } func aesCTRXOR(key, inText, iv []byte) ([]byte, error) { // AES-128 is selected due to size of encryptKey. aesBlock, err := aes.NewCipher(key) if err != nil { return nil, err } stream := cipher.NewCTR(aesBlock, iv) outText := make([]byte, len(inText)) stream.XORKeyStream(outText, inText) return outText, err } func aesCBCDecrypt(key, cipherText, iv []byte) ([]byte, error) { aesBlock, err := aes.NewCipher(key) if err != nil { return nil, err } decrypter := cipher.NewCBCDecrypter(aesBlock, iv) paddedPlaintext := make([]byte, len(cipherText)) decrypter.CryptBlocks(paddedPlaintext, cipherText) plaintext := pkcs7Unpad(paddedPlaintext) if plaintext == nil { return nil, ErrDecrypt } return plaintext, err } // From https://leanpub.com/gocrypto/read#leanpub-auto-block-cipher-modes func pkcs7Unpad(in []byte) []byte { if len(in) == 0 { return nil } padding := in[len(in)-1] if int(padding) > len(in) || padding > aes.BlockSize { return nil } else if padding == 0 { return nil } for i := len(in) - 1; i > len(in)-int(padding)-1; i-- { if in[i] != padding { return nil } } return in[:len(in)-int(padding)] } func RawKeyToCryptoJSON(rawKeyFile []byte) (cj CryptoJSON, e error){ var keyJSON encryptedKeyJSONV3 if e := json.Unmarshal(rawKeyFile, &keyJSON); e != nil { return cj, fmt.Errorf("failed to read key file: %s", e) } return keyJSON.Crypto, e }