Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keystore: entropy vs mnemonic #11

Open
ronaldmannak opened this issue Dec 26, 2021 · 1 comment
Open

Keystore: entropy vs mnemonic #11

ronaldmannak opened this issue Dec 26, 2021 · 1 comment

Comments

@ronaldmannak
Copy link
Contributor

Continuing the discussion on Quill and a Twitter conversation with Alex Komarov of MyEtherWallet re: how to store the HDWallet in Safari Wallet.

We're using the KeystoreV3 code from Essential One's HDWallet library (shown below) with a convenience init in an extension (also shown below). The extension that bridges the gap between the Essential One keystore code and MEWwalletKit. The data to be stored in the keystore is encrypted by a user-defined password.

The file format of the keystore is defined by Essential One's implementation. The question is what do we store in the keystore?

Initially Safari Wallet stored the mnemonic (the 12 word recovery phrase) in the keystore. But with support for multiple languages, it would make opening the file slightly more complicated: the app either has to try to restore the key using multiple languages, or we should store what language was used.

I thought it made sense to store something language-independent so we can recreate the master key and recreate the mnemonic in any language supported. The BIP39.entropy property seemed to fit the bill: it is language independent and allows you to recreate the master key and mnemonic.

However, some developers on the internet advise against storing the entropy in the keystore. I'm not sure why we can't store the entropy. The conversion from entropy to mnemonic will remain consistent, so even if we replace MEWwalletKit with another HDWallet library, the same keys will be generated.

Can we continue to store the entropy in the keychain or is there a good reason to roll back to storing the mnemonic itself in the keystore?

Keystore extension:

import Foundation
import SafariWalletCore
import MEWwalletKit

extension KeystoreV3 {
    
    convenience init(bip39: BIP39, password: String) async throws {
        
        // 1. Create keystore V3
        guard let entropy = bip39.entropy,
              let passwordData = password.data(using: .utf8)?.sha256()
        else {
            throw WalletError.invalidInput(nil)
        }
        try await self.init(privateKey: entropy, passwordData: passwordData)
    }
    
    func save(name: String) async throws {
        let hdWalletFile = try SharedDocument(filename: name.deletingPathExtension().appendPathExtension(KEYSTORE_FILE_EXTENSION))
        try await hdWalletFile.write(try self.encodedData())
    }
 
    static func load(name: String, password: String? = nil, language: BIP39Wordlist = .english) async throws -> BIP39 {
        
        // 1. Load keystore
        let keystoreData = try await SharedDocument(filename: name.deletingPathExtension().appendPathExtension(KEYSTORE_FILE_EXTENSION)).read()
        let keystore = try KeystoreV3(keystore: keystoreData)
        
        // 2. Fetch password from keychain
        let pw: String
        if let password = password {
            pw = password
        } else {
            let passwordItem = KeychainPasswordItem(service: KeychainConfiguration.serviceName, account: name.deletingPathExtension(), accessGroup: KeychainConfiguration.accessGroup)
            pw = try passwordItem.readPassword()
        }
        guard let pwData = pw.data(using: .utf8)?.sha256() else {
            throw WalletError.invalidPassword
        }
        
        // 3. Decrypt entropy
        guard let entropy = try await keystore.getDecryptedKeystore(passwordData: pwData) else {
            throw WalletError.wrongPassword
        }
        return BIP39(entropy: entropy, language: language)
    }
}

Original KeystoreV3 code from EssentialOne:

//
//  KeystoreV3.swift
//  HDWalletKit
//
//  Created by Pavlo Boiko on 20.08.18.
//  Copyright © 2018 Essentia. All rights reserved.
//

import Foundation
import CryptoSwift

public enum KeystoreError: Error {
    case keyDerivationError
    case aesError
    case randomBytesErrror
}

public class KeystoreV3 {
    
    public var keystoreParams: KeystoreParamsV3?
        
    ///
    public required init (privateKey: Data, passwordData: Data) async throws {
        self.keystoreParams = try await encryptDataToStorage(passwordData, data: privateKey)
    }
    
    ///
    public required init (keystore: Data) throws {
        self.keystoreParams = try JSONDecoder().decode(KeystoreParamsV3.self, from: keystore)
    }

    public func encodedData() throws -> Data {
        return try JSONEncoder().encode(keystoreParams)
    }
    
    /// Decode keystore with password
    ///
    /// - Parameter password: encoded password
    /// - Returns: decrypted keystore value
    /// - Throws: wrong password error
    public func getDecryptedKeystore(passwordData: Data) async throws -> Data? {
        guard let keystoreParams = self.keystoreParams else {return nil}
        guard let saltData = Data.fromHex(keystoreParams.crypto.kdfparams.salt) else {return nil}
        let derivedLen = keystoreParams.crypto.kdfparams.dklen
        guard let N = keystoreParams.crypto.kdfparams.n else {return nil}
        guard let P = keystoreParams.crypto.kdfparams.p else {return nil}
        guard let R = keystoreParams.crypto.kdfparams.r else {return nil}
        guard let derivedKey = encryptData(passwordData: passwordData,
                                           salt: saltData,
                                           length: derivedLen,
                                           N: N,
                                           R: R,
                                           P: P) else {return nil}
        var dataForMAC = Data()
        let derivedKeyLast16bytes = Data(derivedKey[(derivedKey.count - 16)...(derivedKey.count - 1)])
        dataForMAC.append(derivedKeyLast16bytes)
        guard let cipherText = Data.fromHex(keystoreParams.crypto.ciphertext) else {return nil}
        dataForMAC.append(cipherText)
        let mac = dataForMAC.sha3(.keccak256)
        guard let calculatedMac = Data.fromHex(keystoreParams.crypto.mac),
            mac.constantTimeComparisonTo(calculatedMac) else {return nil}
        let decryptionKey = derivedKey[0...15]
        guard let IV = Data.fromHex(keystoreParams.crypto.cipherparams.iv) else {return nil}
        guard let aesCipher = try? AES(key: decryptionKey.bytes, blockMode: CTR(iv: IV.bytes), padding: .noPadding) else {return nil}
        guard let decryptedPK:Array<UInt8> = try? aesCipher.decrypt(cipherText.bytes) else { return nil }
        return Data(decryptedPK)
    }
    
    private func encryptData(passwordData: Data, salt: Data, length: Int, N: Int, R: Int, P: Int) -> Data? {
        guard let deriver = try? Scrypt(password: passwordData.bytes, salt: salt.bytes, dkLen: length, N: N, r: R, p: P) else {return nil}
        guard let result = try? deriver.calculate() else {return nil}
        return Data(result)
    }
    
    private func encryptDataToStorage(_ passwordData: Data, data: Data, dkLen: Int=32, N: Int = 1024, R: Int = 8, P: Int = 1) async throws -> KeystoreParamsV3 {
        let saltLen = 32;
        guard let saltData = Data.randomBytes(length: saltLen) else { throw KeystoreError.randomBytesErrror }
        guard let derivedKey = encryptData(passwordData: passwordData,
                                           salt: saltData,
                                           length: dkLen,
                                           N: N,
                                           R: R,
                                           P: P) else {throw KeystoreError.keyDerivationError}
        let last16bytes = Data(derivedKey[(derivedKey.count - 16)...(derivedKey.count-1)])
        let encryptionKey = Data(derivedKey[0...15])
        guard let IV = Data.randomBytes(length: 16) else { throw KeystoreError.randomBytesErrror }
        let aesCipher = try? AES(key: encryptionKey.bytes, blockMode: CTR(iv: IV.bytes), padding: .noPadding)
        guard let encryptedKey = try aesCipher?.encrypt(data.bytes) else { throw KeystoreError.aesError }
        let encryptedKeyData = Data(encryptedKey)
        var dataForMAC = Data()
        dataForMAC.append(last16bytes)
        dataForMAC.append(encryptedKeyData)
        let mac = dataForMAC.sha3(.keccak256)
        
        let kdfparams = KeystoreParamsV3.CryptoParamsV3.KdfParamsV3(salt: saltData.toHexString(), dklen: dkLen, n: N, p: P, r: R)
        let cipherparams = KeystoreParamsV3.CryptoParamsV3.CipherParamsV3(iv: IV.toHexString())
        let crypto = KeystoreParamsV3.CryptoParamsV3(ciphertext: encryptedKeyData.toHexString(), cipher: "aes-128-ctr", cipherparams: cipherparams, kdf: "scrypt", kdfparams: kdfparams, mac: mac.toHexString(), version: nil)
        return KeystoreParamsV3(crypto: crypto, id: UUID().uuidString.lowercased(), version: 3)
    }
}
@lopanism
Copy link

lopanism commented Feb 2, 2022

Excellent question @ronaldmannak.

In short what you are asking is:
"Can we continue to store the entropy in the keychain or is there a good reason to roll back to storing the mnemonic itself in the keystore?"

Here's what we think.

What to store: we don’t see any compelling reason why should one store actual mnemonic - the whole idea of a mnemonic is to be human readable, so its best to use the machine friendly representation that is more succinct and language agnostic.

How and where to store: since keystore file seems to be the only viable option in your case, the question is how to encrypt its contents and where to store it. We think its best to store it in keychain, and encrypt it with a key generated in a secure enclave, that way you should get security benefits of both, enclave and keychain. The only caveat is that when the user will be changing their phones, they will need to restore their account using their recovery phrase.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants