Skip to content

Commit 8ae45e1

Browse files
Merge pull request #833 from JeneaVranceanu/feat/eip712-dynamic-parsing
feat(EIP712): parsing of TypedData payload; encoding + hashing;
2 parents ef28c1f + 6cc06db commit 8ae45e1

File tree

16 files changed

+1199
-118
lines changed

16 files changed

+1199
-118
lines changed

Sources/Web3Core/EthereumABI/ABIParsing.swift

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,41 @@ import Foundation
77

88
extension ABI {
99

10-
public enum ParsingError: Swift.Error {
10+
public enum ParsingError: LocalizedError {
1111
case invalidJsonFile
12-
case elementTypeInvalid
12+
case elementTypeInvalid(_ desc: String? = nil)
1313
case elementNameInvalid
1414
case functionInputInvalid
1515
case functionOutputInvalid
1616
case eventInputInvalid
1717
case parameterTypeInvalid
1818
case parameterTypeNotFound
1919
case abiInvalid
20+
21+
public var errorDescription: String? {
22+
var errorMessage: [String?]
23+
switch self {
24+
case .invalidJsonFile:
25+
errorMessage = ["invalidJsonFile"]
26+
case .elementTypeInvalid(let desc):
27+
errorMessage = ["elementTypeInvalid", desc]
28+
case .elementNameInvalid:
29+
errorMessage = ["elementNameInvalid"]
30+
case .functionInputInvalid:
31+
errorMessage = ["functionInputInvalid"]
32+
case .functionOutputInvalid:
33+
errorMessage = ["functionOutputInvalid"]
34+
case .eventInputInvalid:
35+
errorMessage = ["eventInputInvalid"]
36+
case .parameterTypeInvalid:
37+
errorMessage = ["parameterTypeInvalid"]
38+
case .parameterTypeNotFound:
39+
errorMessage = ["parameterTypeNotFound"]
40+
case .abiInvalid:
41+
errorMessage = ["abiInvalid"]
42+
}
43+
return errorMessage.compactMap { $0 }.joined(separator: " ")
44+
}
2045
}
2146

2247
enum TypeParsingExpressions {
@@ -39,7 +64,7 @@ extension ABI.Record {
3964
public func parse() throws -> ABI.Element {
4065
let typeString = self.type ?? "function"
4166
guard let type = ABI.ElementType(rawValue: typeString) else {
42-
throw ABI.ParsingError.elementTypeInvalid
67+
throw ABI.ParsingError.elementTypeInvalid("Invalid ABI type \(typeString).")
4368
}
4469
return try parseToElement(from: self, type: type)
4570
}

Sources/Web3Core/EthereumABI/ABITypeParser.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public struct ABITypeParser {
4646

4747
public static func parseTypeString(_ string: String) throws -> ABI.Element.ParameterType {
4848
let (type, tail) = recursiveParseType(string)
49-
guard let t = type, tail == nil else {throw ABI.ParsingError.elementTypeInvalid}
49+
guard let t = type, tail == nil else { throw ABI.ParsingError.elementTypeInvalid("Invalid ABI type \(string).") }
5050
return t
5151
}
5252

Sources/Web3Core/KeystoreManager/AbstractKeystore.swift

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,30 @@ public protocol AbstractKeystore {
1111
func UNSAFE_getPrivateKeyData(password: String, account: EthereumAddress) throws -> Data
1212
}
1313

14-
public enum AbstractKeystoreError: Error {
15-
case noEntropyError
16-
case keyDerivationError
17-
case aesError
18-
case invalidAccountError
14+
public enum AbstractKeystoreError: LocalizedError {
15+
case noEntropyError(_ additionalDescription: String? = nil)
16+
case keyDerivationError(_ additionalDescription: String? = nil)
17+
case aesError(_ additionalDescription: String? = nil)
18+
case invalidAccountError(_ additionalDescription: String? = nil)
1919
case invalidPasswordError
20-
case encryptionError(String)
20+
case encryptionError(_ additionalDescription: String? = nil)
21+
22+
public var errorDescription: String? {
23+
var errorMessage: [String?]
24+
switch self {
25+
case .noEntropyError(let additionalDescription):
26+
errorMessage = ["Entropy error (e.g. failed to generate a random array of bytes).", additionalDescription]
27+
case .keyDerivationError(let additionalDescription):
28+
errorMessage = ["Key derivation error.", additionalDescription]
29+
case .aesError(let additionalDescription):
30+
errorMessage = ["AES error.", additionalDescription]
31+
case .invalidAccountError(let additionalDescription):
32+
errorMessage = ["Invalid account error.", additionalDescription]
33+
case .invalidPasswordError:
34+
errorMessage = ["Invalid password error."]
35+
case .encryptionError(let additionalDescription):
36+
errorMessage = ["Encryption error.", additionalDescription]
37+
}
38+
return errorMessage.compactMap { $0 }.joined(separator: " ")
39+
}
2140
}

Sources/Web3Core/KeystoreManager/BIP32Keystore.swift

Lines changed: 57 additions & 58 deletions
Large diffs are not rendered by default.

Sources/Web3Core/KeystoreManager/BIP39.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,13 @@ public class BIP39 {
9595
}
9696

9797
private static func entropyOf(size: Int) throws -> Data {
98+
let isCorrectSize = size >= 128 && size <= 256 && size.isMultiple(of: 32)
99+
let randomBytesCount = size / 8
98100
guard
99-
size >= 128 && size <= 256 && size.isMultiple(of: 32),
100-
let entropy = Data.randomBytes(length: size/8)
101+
isCorrectSize,
102+
let entropy = Data.randomBytes(length: randomBytesCount)
101103
else {
102-
throw AbstractKeystoreError.noEntropyError
104+
throw AbstractKeystoreError.noEntropyError("BIP39. \(!isCorrectSize ? "Requested entropy of wrong bits size: \(size). Expected: 128 <= size <= 256, size % 32 == 0." : "Failed to generate \(randomBytesCount) of random bytes.")")
103105
}
104106
return entropy
105107
}

Sources/Web3Core/KeystoreManager/EthereumKeystoreV3.swift

Lines changed: 27 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@ public class EthereumKeystoreV3: AbstractKeystore {
2323
}
2424

2525
public func UNSAFE_getPrivateKeyData(password: String, account: EthereumAddress) throws -> Data {
26-
if self.addresses?.count == 1 && account == self.addresses?.last {
27-
guard let privateKey = try? self.getKeyData(password) else {
26+
if account == addresses?.last {
27+
guard let privateKey = try? getKeyData(password) else {
2828
throw AbstractKeystoreError.invalidPasswordError
2929
}
3030
return privateKey
3131
}
32-
throw AbstractKeystoreError.invalidAccountError
32+
throw AbstractKeystoreError.invalidAccountError("EthereumKeystoreV3. Cannot get private key: keystore doesn't contain information about given address \(account.address).")
3333
}
3434

3535
// Class
@@ -77,7 +77,7 @@ public class EthereumKeystoreV3: AbstractKeystore {
7777
defer {
7878
Data.zero(&newPrivateKey)
7979
}
80-
try encryptDataToStorage(password, keyData: newPrivateKey, aesMode: aesMode)
80+
try encryptDataToStorage(password, privateKey: newPrivateKey, aesMode: aesMode)
8181
}
8282

8383
public init?(privateKey: Data, password: String, aesMode: String = "aes-128-cbc") throws {
@@ -87,68 +87,60 @@ public class EthereumKeystoreV3: AbstractKeystore {
8787
guard SECP256K1.verifyPrivateKey(privateKey: privateKey) else {
8888
return nil
8989
}
90-
try encryptDataToStorage(password, keyData: privateKey, aesMode: aesMode)
90+
try encryptDataToStorage(password, privateKey: privateKey, aesMode: aesMode)
9191
}
9292

93-
fileprivate func encryptDataToStorage(_ password: String, keyData: Data?, dkLen: Int = 32, N: Int = 4096, R: Int = 6, P: Int = 1, aesMode: String = "aes-128-cbc") throws {
94-
if keyData == nil {
95-
throw AbstractKeystoreError.encryptionError("Encryption without key data")
93+
fileprivate func encryptDataToStorage(_ password: String, privateKey: Data, dkLen: Int = 32, N: Int = 4096, R: Int = 6, P: Int = 1, aesMode: String = "aes-128-cbc") throws {
94+
if privateKey.count != 32 {
95+
throw AbstractKeystoreError.encryptionError("EthereumKeystoreV3. Attempted encryption with private key of length != 32. Given private key length is \(privateKey.count).")
9696
}
9797
let saltLen = 32
9898
guard let saltData = Data.randomBytes(length: saltLen) else {
99-
throw AbstractKeystoreError.noEntropyError
99+
throw AbstractKeystoreError.noEntropyError("EthereumKeystoreV3. Failed to generate random bytes: `Data.randomBytes(length: \(saltLen))`.")
100100
}
101101
guard let derivedKey = scrypt(password: password, salt: saltData, length: dkLen, N: N, R: R, P: P) else {
102-
throw AbstractKeystoreError.keyDerivationError
102+
throw AbstractKeystoreError.keyDerivationError("EthereumKeystoreV3. Scrypt function failed.")
103103
}
104104
let last16bytes = Data(derivedKey[(derivedKey.count - 16)...(derivedKey.count - 1)])
105105
let encryptionKey = Data(derivedKey[0...15])
106106
guard let IV = Data.randomBytes(length: 16) else {
107-
throw AbstractKeystoreError.noEntropyError
107+
throw AbstractKeystoreError.noEntropyError("EthereumKeystoreV3. Failed to generate random bytes: `Data.randomBytes(length: 16)`.")
108108
}
109-
var aesCipher: AES?
110-
switch aesMode {
109+
var aesCipher: AES
110+
switch aesMode.lowercased() {
111111
case "aes-128-cbc":
112-
aesCipher = try? AES(key: encryptionKey.bytes, blockMode: CBC(iv: IV.bytes), padding: .noPadding)
112+
aesCipher = try AES(key: encryptionKey.bytes, blockMode: CBC(iv: IV.bytes), padding: .noPadding)
113113
case "aes-128-ctr":
114-
aesCipher = try? AES(key: encryptionKey.bytes, blockMode: CTR(iv: IV.bytes), padding: .noPadding)
114+
aesCipher = try AES(key: encryptionKey.bytes, blockMode: CTR(iv: IV.bytes), padding: .noPadding)
115115
default:
116-
aesCipher = nil
116+
throw AbstractKeystoreError.aesError("EthereumKeystoreV3. AES error: given AES mode can be one of 'aes-128-cbc' or 'aes-128-ctr'. Instead '\(aesMode)' was given.")
117117
}
118-
if aesCipher == nil {
119-
throw AbstractKeystoreError.aesError
120-
}
121-
guard let encryptedKey = try aesCipher?.encrypt(keyData!.bytes) else {
122-
throw AbstractKeystoreError.aesError
123-
}
124-
let encryptedKeyData = Data(encryptedKey)
125-
var dataForMAC = Data()
126-
dataForMAC.append(last16bytes)
127-
dataForMAC.append(encryptedKeyData)
118+
119+
let encryptedKeyData = Data(try aesCipher.encrypt(privateKey.bytes))
120+
let dataForMAC = last16bytes + encryptedKeyData
128121
let mac = dataForMAC.sha3(.keccak256)
129122
let kdfparams = KdfParamsV3(salt: saltData.toHexString(), dklen: dkLen, n: N, p: P, r: R, c: nil, prf: nil)
130123
let cipherparams = CipherParamsV3(iv: IV.toHexString())
131124
let crypto = CryptoParamsV3(ciphertext: encryptedKeyData.toHexString(), cipher: aesMode, cipherparams: cipherparams, kdf: "scrypt", kdfparams: kdfparams, mac: mac.toHexString(), version: nil)
132-
guard let pubKey = Utilities.privateToPublic(keyData!) else {
133-
throw AbstractKeystoreError.keyDerivationError
125+
guard let publicKey = Utilities.privateToPublic(privateKey) else {
126+
throw AbstractKeystoreError.keyDerivationError("EthereumKeystoreV3. Failed to derive public key from given private key. `Utilities.privateToPublic(privateKey)` returned `nil`.")
134127
}
135-
guard let addr = Utilities.publicToAddress(pubKey) else {
136-
throw AbstractKeystoreError.keyDerivationError
128+
guard let addr = Utilities.publicToAddress(publicKey) else {
129+
throw AbstractKeystoreError.keyDerivationError("EthereumKeystoreV3. Failed to derive address from derived public key. `Utilities.publicToAddress(publicKey)` returned `nil`.")
137130
}
138131
self.address = addr
139132
let keystoreparams = KeystoreParamsV3(address: addr.address.lowercased(), crypto: crypto, id: UUID().uuidString.lowercased(), version: 3)
140133
self.keystoreParams = keystoreparams
141134
}
142135

143136
public func regenerate(oldPassword: String, newPassword: String, dkLen: Int = 32, N: Int = 4096, R: Int = 6, P: Int = 1) throws {
144-
var keyData = try self.getKeyData(oldPassword)
145-
if keyData == nil {
146-
throw AbstractKeystoreError.encryptionError("Failed to decrypt a keystore")
137+
guard var privateKey = try getKeyData(oldPassword) else {
138+
throw AbstractKeystoreError.encryptionError("EthereumKeystoreV3. Failed to decrypt a keystore")
147139
}
148140
defer {
149-
Data.zero(&keyData!)
141+
Data.zero(&privateKey)
150142
}
151-
try self.encryptDataToStorage(newPassword, keyData: keyData!, aesMode: self.keystoreParams!.crypto.cipher)
143+
try self.encryptDataToStorage(newPassword, privateKey: privateKey, aesMode: self.keystoreParams!.crypto.cipher)
152144
}
153145

154146
fileprivate func getKeyData(_ password: String) throws -> Data? {

Sources/Web3Core/KeystoreManager/KeystoreManager.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public class KeystoreManager: AbstractKeystore {
4343

4444
public func UNSAFE_getPrivateKeyData(password: String, account: EthereumAddress) throws -> Data {
4545
guard let keystore = walletForAddress(account) else {
46-
throw AbstractKeystoreError.invalidAccountError
46+
throw AbstractKeystoreError.invalidAccountError("KeystoreManager: no keystore/wallet found for given address. Address `\(account.address)`.")
4747
}
4848
return try keystore.UNSAFE_getPrivateKeyData(password: password, account: account)
4949
}

Sources/Web3Core/Transaction/CodableTransaction.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ public struct CodableTransaction {
156156
let result = self.attemptSignature(privateKey: privateKey, useExtraEntropy: useExtraEntropy)
157157
if result { return }
158158
}
159-
throw AbstractKeystoreError.invalidAccountError
159+
throw AbstractKeystoreError.invalidAccountError("Failed to sign transaction with given private key.")
160160
}
161161

162162
// actual signing algorithm implementation

Sources/web3swift/Utils/EIP/EIP712.swift renamed to Sources/web3swift/Utils/EIP/EIP712/EIP712.swift

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ public class EIP712 {
1616
public typealias Bytes = Data
1717
}
1818

19+
// FIXME: this type is wrong - The minimum number of optional fields is 5, and those are
20+
// string name the user readable name of signing domain, i.e. the name of the DApp or the protocol.
21+
// string version the current major version of the signing domain. Signatures from different versions are not compatible.
22+
// uint256 chainId the EIP-155 chain id. The user-agent should refuse signing if it does not match the currently active chain.
23+
// address verifyingContract the address of the contract that will verify the signature. The user-agent may do contract specific phishing prevention.
24+
// bytes32 salt an disambiguating salt for the protocol. This can be used as a domain separator of last resort.
1925
public struct EIP712Domain: EIP712Hashable {
2026
public let chainId: EIP712.UInt256?
2127
public let verifyingContract: EIP712.Address
@@ -54,6 +60,8 @@ public extension EIP712Hashable {
5460
result = ABIEncoder.encodeSingleType(type: .uint(bits: 256), value: field)!
5561
case is EIP712.Address:
5662
result = ABIEncoder.encodeSingleType(type: .address, value: field)!
63+
case let boolean as Bool:
64+
result = ABIEncoder.encodeSingleType(type: .uint(bits: 8), value: boolean ? 1 : 0)!
5765
case let hashable as EIP712Hashable:
5866
result = try hashable.hash()
5967
default:
@@ -64,16 +72,19 @@ public extension EIP712Hashable {
6472
preconditionFailure("Not solidity type")
6573
}
6674
}
67-
guard result.count == 32 else { preconditionFailure("ABI encode error") }
75+
guard result.count % 32 == 0 else { preconditionFailure("ABI encode error") }
6876
parameters.append(result)
6977
}
7078
return Data(parameters.flatMap { $0.bytes }).sha3(.keccak256)
7179
}
7280
}
7381

74-
public func eip712encode(domainSeparator: EIP712Hashable, message: EIP712Hashable) throws -> Data {
75-
let data = try Data([UInt8(0x19), UInt8(0x01)]) + domainSeparator.hash() + message.hash()
76-
return data.sha3(.keccak256)
82+
public func eip712hash(domainSeparator: EIP712Hashable, message: EIP712Hashable) throws -> Data {
83+
try eip712hash(domainSeparatorHash: domainSeparator.hash(), messageHash: message.hash())
84+
}
85+
86+
public func eip712hash(domainSeparatorHash: Data, messageHash: Data) -> Data {
87+
(Data([UInt8(0x19), UInt8(0x01)]) + domainSeparatorHash + messageHash).sha3(.keccak256)
7788
}
7889

7990
// MARK: - Additional private and public extensions with support members

0 commit comments

Comments
 (0)