Skip to content

Commit 4038fcc

Browse files
committed
feat: create eth2 keystores
1 parent 106621a commit 4038fcc

File tree

4 files changed

+167
-11
lines changed

4 files changed

+167
-11
lines changed

Package.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ let package = Package(
2222
.package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", from: "1.8.1"),
2323
.package(url: "https://github.com/Boilertalk/secp256k1.swift.git", from: "0.1.7"),
2424
.package(name: "Scrypt", url: "https://github.com/greymass/swift-scrypt.git", from: "1.0.0"),
25+
.package(url: "https://github.com/MyEtherWallet/bls-eth-swift.git", .upToNextMajor(from: "1.0.0")),
2526

2627
// Test dependencies
2728
.package(url: "https://github.com/Quick/Quick.git", from: "5.0.1"),
@@ -34,6 +35,7 @@ let package = Package(
3435
.product(name: "CryptoSwift", package: "CryptoSwift"),
3536
.product(name: "secp256k1", package: "secp256k1.swift"),
3637
.product(name: "Scrypt", package: "Scrypt"),
38+
.product(name: "bls-eth-swift", package: "bls-eth-swift"),
3739
],
3840
path: "Sources",
3941
sources: ["Keystore"]),

Sources/Keystore/KeystoreETH2.swift

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,28 @@ public struct KeystoreETH2: Codable {
66

77
// MARK: - Public API
88

9-
/// Creates a new keystore for the given private key and password.
9+
/// Creates a keystore for the given privateKey with the given password.
1010
///
1111
/// - parameter privateKey: The private key to encrypt.
1212
/// - parameter password: The password to use for the encryption.
13+
/// - parameter kdf: The key derivation function to use.
14+
/// - parameter cipher: The cipher to use for encryption.
15+
/// - parameter checksum: The password checksum to use to detect bad passwords during decryption.
16+
/// - parameter rounds: The number of rounds for the key derivation function to use. Defaults to a secure number.
1317
///
14-
/// - throws: Error if any step fails.
15-
// public init(privateKey: [UInt8], password: String, kdf: Keystore.Crypto.KDFType = .scrypt, cipher: IVBlockModeType = .ctr, rounds: Int? = nil) throws {
16-
// self = try KeystoreFactory.keystore(from: privateKey, password: password, kdf: kdf, cipher: cipher, rounds: rounds)
17-
// }
18+
/// - returns: The KeystoreETH2 object with the encrypted private key.
19+
///
20+
/// - throws: Some `KeystoreETH2Factory.Error` if any step fails.
21+
public init(
22+
from privateKey: [UInt8],
23+
password: String,
24+
kdf: KeystoreETH2.KDFModule.KDFType,
25+
cipher: KeystoreETH2.CipherModule.CipherType,
26+
checksum: KeystoreETH2.ChecksumModule.ChecksumType,
27+
rounds: Int = 262144
28+
) throws {
29+
self = try KeystoreETH2Factory.keystore(from: privateKey, password: password, kdf: kdf, cipher: cipher, checksum: checksum, rounds: rounds)
30+
}
1831

1932
/// Extracts the private key from this keystore with the given password.
2033
///

Sources/Keystore/KeystoreETH2Factory.swift

Lines changed: 113 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Foundation
22
import CryptoSwift
33
import Scrypt
4+
import bls_framework
45

56
public struct KeystoreETH2Factory {
67

@@ -11,7 +12,7 @@ public struct KeystoreETH2Factory {
1112
///
1213
/// - returns: The extracted private key.
1314
///
14-
/// - throws: Some `KeystoreFactory.Error` if any step fails.
15+
/// - throws: Some `KeystoreETH2Factory.Error` if any step fails.
1516
public static func privateKey(from keystore: KeystoreETH2, password: String) throws -> Array<UInt8> {
1617
var forbiddenCharacterSet = CharacterSet()
1718
forbiddenCharacterSet.insert(charactersIn: Unicode.Scalar(0x00)...Unicode.Scalar(0x1f))
@@ -63,6 +64,102 @@ public struct KeystoreETH2Factory {
6364
return try aes.decrypt([UInt8](ciphertextData))
6465
}
6566

67+
/// Creates a keystore for the given privateKey with the given password.
68+
///
69+
/// - parameter privateKey: The private key to encrypt.
70+
/// - parameter password: The password to use for the encryption.
71+
/// - parameter kdf: The key derivation function to use.
72+
/// - parameter cipher: The cipher to use for encryption.
73+
/// - parameter checksum: The password checksum to use to detect bad passwords during decryption.
74+
/// - parameter rounds: The number of rounds for the key derivation function to use. Defaults to a secure number.
75+
///
76+
/// - returns: The KeystoreETH2 object with the encrypted private key.
77+
///
78+
/// - throws: Some `KeystoreETH2Factory.Error` if any step fails.
79+
public static func keystore(
80+
from privateKey: [UInt8],
81+
password: String,
82+
kdf: KeystoreETH2.KDFModule.KDFType,
83+
cipher: KeystoreETH2.CipherModule.CipherType,
84+
checksum: KeystoreETH2.ChecksumModule.ChecksumType,
85+
rounds: Int = 262144
86+
) throws -> KeystoreETH2 {
87+
guard privateKey.count == 32 else {
88+
throw Error.privateKeyMalformed
89+
}
90+
91+
guard let iv = [UInt8].secureRandom(count: 16), let salt = [UInt8].secureRandom(count: 32) else {
92+
throw Error.bytesGenerationFailed
93+
}
94+
95+
let password = password.decomposedStringWithCompatibilityMapping
96+
.components(separatedBy: .controlCharacters)
97+
.filter({ !$0.isEmpty })
98+
.joined(separator: "")
99+
100+
// Derive key
101+
let kdfModule: KeystoreETH2.KDFModule
102+
switch kdf {
103+
case .scrypt:
104+
kdfModule = .init(function: kdf, params: .init(salt: salt.toHexString(), dklen: 32, n: rounds, r: 8, p: 1), message: "")
105+
case .pbkdf2:
106+
kdfModule = .init(function: kdf, params: .init(salt: salt.toHexString(), dklen: 32, prf: "hmac-sha256", c: rounds), message: "")
107+
}
108+
let encryptionKey = try deriveKey(password: password, kdf: kdfModule)
109+
guard encryptionKey.count >= 32 else {
110+
throw Error.kdfFailed
111+
}
112+
113+
// Encrypt
114+
let usableKey = encryptionKey[0..<16]
115+
116+
guard cipher == .aes128Ctr else {
117+
throw Error.cipherNotAvailable
118+
}
119+
120+
let aes = try AES(
121+
key: [UInt8](usableKey),
122+
blockMode: CTR(iv: iv),
123+
padding: .noPadding
124+
)
125+
let cipherMessage = try aes.encrypt(privateKey)
126+
let cipherModule = KeystoreETH2.CipherModule(function: cipher, params: .init(iv: iv.toHexString()), message: cipherMessage.toHexString())
127+
128+
// Checksum
129+
guard checksum == .sha256 else {
130+
throw Error.checksumNotAvailable
131+
}
132+
133+
let checksumGenerated = try generatePasswordChecksum(decryptionKey: encryptionKey, cipher: cipherModule)
134+
let checksumModule = KeystoreETH2.ChecksumModule(function: checksum, params: .init(), message: checksumGenerated.toHexString())
135+
136+
// BLS pubkey
137+
138+
try BLSInterface.blsInit()
139+
140+
var serializedPrivateKey = privateKey
141+
var secretKey = blsSecretKey.init()
142+
if blsSecretKeyDeserialize(&secretKey, &serializedPrivateKey, numericCast(serializedPrivateKey.count)) <= 0 {
143+
throw Error.privateKeyMalformed
144+
}
145+
var publicKey = blsPublicKey.init()
146+
blsGetPublicKey(&publicKey, &secretKey)
147+
// Ethereum public key is 48 bytes
148+
var publicKeyBytes = Data(count: 48).bytes
149+
blsPublicKeySerialize(&publicKeyBytes, 48, &publicKey)
150+
151+
let ethereumPublicKey = Data(publicKeyBytes)
152+
153+
return KeystoreETH2(
154+
crypto: .init(kdf: kdfModule, checksum: checksumModule, cipher: cipherModule),
155+
description: "",
156+
pubkey: ethereumPublicKey.toHexString(),
157+
path: "",
158+
uuid: UUID().uuidString,
159+
version: 4
160+
)
161+
}
162+
66163
private static func deriveKey(
67164
password: String,
68165
kdf: KeystoreETH2.KDFModule
@@ -104,17 +201,25 @@ public struct KeystoreETH2Factory {
104201
).calculate())
105202
}
106203

107-
private static func passwordVerification(
204+
private static func generatePasswordChecksum(
108205
decryptionKey: Data,
109-
cipher: KeystoreETH2.CipherModule,
110-
checksum: KeystoreETH2.ChecksumModule
111-
) throws -> Bool {
206+
cipher: KeystoreETH2.CipherModule
207+
) throws -> Data {
112208
let dkSlice = decryptionKey[16..<32]
113209
var preImage = dkSlice
114210
try preImage.append(contentsOf: cipher.message.dataWithHexString())
115211

116212
let calculatedChecksum = preImage.sha256()
117213

214+
return calculatedChecksum
215+
}
216+
217+
private static func passwordVerification(
218+
decryptionKey: Data,
219+
cipher: KeystoreETH2.CipherModule,
220+
checksum: KeystoreETH2.ChecksumModule
221+
) throws -> Bool {
222+
let calculatedChecksum = try generatePasswordChecksum(decryptionKey: decryptionKey, cipher: cipher)
118223
return try calculatedChecksum == checksum.message.dataWithHexString()
119224
}
120225

@@ -141,6 +246,9 @@ public struct KeystoreETH2Factory {
141246
/// The given cipher is not available
142247
case cipherNotAvailable
143248

249+
/// The given checksum is not available
250+
case checksumNotAvailable
251+
144252
/// Generating random bytes failed
145253
case bytesGenerationFailed
146254

Tests/KeystoreTests/KeystoreETH2Tests/ETH2PrivateKeyExtractionTests.swift

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class ETH2PrivateKeyExtractionTests: QuickSpec {
2424
"eth2_scrypt_test1"
2525
]
2626

27+
// decrypt
2728
for testName in stubTests {
2829
guard let testData = loadStub(named: testName), let test = try? decoder.decode(KeystoreETH2Stub.self, from: testData) else {
2930
it("should never happen") {
@@ -39,7 +40,39 @@ class ETH2PrivateKeyExtractionTests: QuickSpec {
3940

4041
let actualPrivateKey = try? [UInt8](test.privateKey.dataWithHexString())
4142
it("should extract the private key correctly") {
42-
expect(privateKey) == actualPrivateKey
43+
expect(privateKey!) == actualPrivateKey
44+
}
45+
}
46+
47+
// encrypt then decrypt
48+
for testName in stubTests {
49+
guard let testData = loadStub(named: testName), let test = try? decoder.decode(KeystoreETH2Stub.self, from: testData) else {
50+
it("should never happen") {
51+
fail("Stub \(testName) couldn't be loaded")
52+
}
53+
return
54+
}
55+
56+
let encryptedKeystore = try? KeystoreETH2(
57+
from: test.privateKey.dataWithHexString().bytes,
58+
password: test.password,
59+
kdf: test.keystore.crypto.kdf.function,
60+
cipher: test.keystore.crypto.cipher.function,
61+
checksum: test.keystore.crypto.checksum.function,
62+
rounds: test.keystore.crypto.kdf.params.n ?? test.keystore.crypto.kdf.params.c ?? 262144
63+
)
64+
it("should not be nil") {
65+
expect(encryptedKeystore).toNot(beNil())
66+
}
67+
68+
let decryptedPrivateKey = try? encryptedKeystore!.privateKey(password: test.password)
69+
it("should not be nil") {
70+
expect(decryptedPrivateKey).toNot(beNil())
71+
}
72+
73+
let actualPrivateKey = try? [UInt8](test.privateKey.dataWithHexString())
74+
it("should extract the private key correctly") {
75+
expect(decryptedPrivateKey!) == actualPrivateKey
4376
}
4477
}
4578
}

0 commit comments

Comments
 (0)