status-im/status-go

View on GitHub
eth-node/keystore/passphrase.go

Summary

Maintainability
A
0 mins
Test Coverage
F
47%
// 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/google/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)

    id, err := uuid.FromBytes(keyId)
    if err != nil {
        return nil, err
    }

    return &types.Key{
        ID:              id,
        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)
    }
    id, err := uuid.Parse(keyProtected.Id)
    if err != nil {
        return nil, nil, err
    }
    keyId = 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) {
    id, err := uuid.Parse(keyProtected.Id)
    if err != nil {
        return nil, nil, err
    }
    keyId = 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
}