diff --git a/web3sTests/Utils/KeyUtilTests.swift b/web3sTests/Utils/KeyUtilTests.swift index fd7293e0..8f1f50a3 100644 --- a/web3sTests/Utils/KeyUtilTests.swift +++ b/web3sTests/Utils/KeyUtilTests.swift @@ -51,4 +51,12 @@ class KeyUtilTests: XCTestCase { XCTAssertEqual(address.value, "0x751e735a83a8142c1b9dc722ef559b898f1d77fa") } + func testRecoverPublicKey() { + let account = try! EthereumAccount(keyStorage: TestEthereumKeyStorage(privateKey: "0x2639f727ded571d584643895d43d02a7a190f8249748a2c32200cfc12dde7173")) + let signature = try! account.sign(message: "Hello message!") + + let address = try! KeyUtil.recoverPublicKey(message: "Hello message!".web3.keccak256, signature: signature) + + XCTAssertEqual(address, account.address.value.lowercased()) + } } diff --git a/web3swift/src/Utils/KeyUtil.swift b/web3swift/src/Utils/KeyUtil.swift index cabe7c6f..217752b6 100644 --- a/web3swift/src/Utils/KeyUtil.swift +++ b/web3swift/src/Utils/KeyUtil.swift @@ -14,10 +14,11 @@ enum KeyUtilError: Error { case privateKeyInvalid case unknownError case signatureFailure + case signatureParseFailure + case badArguments } class KeyUtil { - static func generatePrivateKeyData() -> Data? { return Data.randomOfLength(32) } @@ -105,4 +106,52 @@ class KeyUtil { return signature } + + static func recoverPublicKey(message: Data, signature: Data) throws -> String { + if signature.count != 65 || message.count != 32 { + throw KeyUtilError.badArguments + } + + guard let ctx = secp256k1_context_create(UInt32(SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY)) else { + print("Failed to sign message: invalid context.") + throw KeyUtilError.invalidContext + } + defer { secp256k1_context_destroy(ctx) } + + // get recoverable signature + let signaturePtr = UnsafeMutablePointer.allocate(capacity: 1) + defer { signaturePtr.deallocate() } + + let serializedSignature = Data(signature[0..<64]) + var v = Int32(signature[64]) + if v >= 27 && v <= 30 { + v -= 27 + } else if v >= 31 && v <= 34 { + v -= 31 + } else if v >= 35 && v <= 38 { + v -= 35 + } + + try serializedSignature.withUnsafeBytes { + guard secp256k1_ecdsa_recoverable_signature_parse_compact(ctx, signaturePtr, $0.bindMemory(to: UInt8.self).baseAddress!, v) == 1 else { + print("Failed to parse signature: recoverable ECDSA signature parse failed.") + throw KeyUtilError.signatureParseFailure + } + } + let pubkey = UnsafeMutablePointer.allocate(capacity: 1) + defer { pubkey.deallocate() } + + try message.withUnsafeBytes { + guard secp256k1_ecdsa_recover(ctx, pubkey, signaturePtr, $0.bindMemory(to: UInt8.self).baseAddress!) == 1 else { + throw KeyUtilError.signatureFailure + } + } + var size: Int = 65 + var rv = Data(count: size) + rv.withUnsafeMutableBytes { + secp256k1_ec_pubkey_serialize(ctx, $0.bindMemory(to: UInt8.self).baseAddress!, &size, pubkey, UInt32(SECP256K1_EC_UNCOMPRESSED)) + return + } + return "0x\(rv[1...].web3.keccak256.web3.hexString.suffix(40))" + } }