diff --git a/MEWwalletKitTests/Sources/eips/eip1024/EIP1024Tests.swift b/MEWwalletKitTests/Sources/eips/eip1024/EIP1024Tests.swift index 94d1c67..6faf10e 100644 --- a/MEWwalletKitTests/Sources/eips/eip1024/EIP1024Tests.swift +++ b/MEWwalletKitTests/Sources/eips/eip1024/EIP1024Tests.swift @@ -9,6 +9,7 @@ import Foundation import Quick import Nimble +import MEWwalletTweetNacl @testable import MEWwalletKit class EIP1024Tests: QuickSpec { @@ -17,15 +18,68 @@ class EIP1024Tests: QuickSpec { ephemPublicKey: "FBH1/pAEHOOW14Lu3FWkgV3qOEcuL78Zy+qW1RwzMXQ=", ciphertext: "f8kBcl/NCyf3sybfbwAKk/np2Bzt9lRVkZejr6uh5FgnNlH/ic62DZzy" ) + let recipientPrivateKeyString = "7e5374ec2ef0d91761a6e72fdf8f6ac665519bfdf6da0a2329cf0d804514b816" + private let senderPrivateKey = try! PrivateKeyEth1(seed: Data(hex: "0xc55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04"), network: .ethereum) + override func spec() { describe("salsa decryption") { it("should decrypt the encrypted data") { let message = try? self.encryptedData.decrypt( - privateKey: "7e5374ec2ef0d91761a6e72fdf8f6ac665519bfdf6da0a2329cf0d804514b816" + privateKey: self.recipientPrivateKeyString //"7e5374ec2ef0d91761a6e72fdf8f6ac665519bfdf6da0a2329cf0d804514b816" ) expect(message ?? "").to(equal("My name is Satoshi Buterin")) } } + + describe("test PrivateKeyEth1 extension") { + it("should convert Ethereum keys to curve25519") { + do { + let privateKey = Data([0x7e, 0x53, 0x74, 0xec, 0x2e, 0xf0, 0xd9, 0x17, 0x61, 0xa6, 0xe7, 0x2f, 0xdf, 0x8f, 0x6a, 0xc6, 0x65, 0x51, 0x9b, 0xfd, 0xf6, 0xda, 0x0a, 0x23, 0x29, 0xcf, 0x0d, 0x80, 0x45, 0x14, 0xb8, 0x16]) + let ethPrivateKey = try PrivateKeyEth1(seed: privateKey, network: .ethereum) + let keypair = try TweetNacl.keyPair(fromSecretKey: ethPrivateKey.data()) + + expect(keypair.secretKey.count) == 32 + expect(keypair.publicKey.count) == 32 + expect(String(data: keypair.publicKey.base64EncodedData(), encoding: .utf8)) == "uh9pcAr8Mg+nLj/8x4BPtMM9k925R8aXPAmjHjAV8x8=" + } catch { + fail(error.localizedDescription) + } + } + } + + describe("roundtrip") { + it("should encrypt and then decrypt the data") { + + let recipientEthKey = PrivateKeyEth1(privateKey: Data(hex: self.recipientPrivateKeyString), network: .ethereum) + expect(recipientEthKey.string()!) == "7e5374ec2ef0d91761a6e72fdf8f6ac665519bfdf6da0a2329cf0d804514b816" + + do { + let recipientKeyPair = try TweetNacl.keyPair(fromSecretKey: recipientEthKey.data()) + guard let recipientPublicKeyString = String(data: recipientKeyPair.publicKey.base64EncodedData(), encoding: .utf8) else { + fail("returned nil") + return + } + + let encryptedData = try EthEncryptedData.encrypt(plaintext: "My name is Satoshi Buterin", senderPrivateKey: self.senderPrivateKey, recipientPublicKey: recipientPublicKeyString) + expect(Data(base64Encoded: encryptedData.ephemPublicKey)!.count) == 32 + + + // Decrypt using recipient's private key string + let decryptedByRecipient = try encryptedData.decrypt(privateKey: recipientEthKey) + expect(decryptedByRecipient) == "My name is Satoshi Buterin" + + // Decrypt using recipient's private key + let decryptedByRecipientString = try encryptedData.decrypt(privateKey: self.recipientPrivateKeyString) + expect(decryptedByRecipientString) == "My name is Satoshi Buterin" + + // Decrypt using sender's private key + let decryptedBySender = try encryptedData.decrypt(senderPrivateKey: self.senderPrivateKey, recipientPublicKey: recipientPublicKeyString) + expect(decryptedBySender) == "My name is Satoshi Buterin" + } catch { + fail(error.localizedDescription) + } + } + } } } diff --git a/Package.swift b/Package.swift index 5e23061..6d9baa1 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.4 +// swift-tools-version:5.5 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -22,7 +22,9 @@ let package = Package( .package(url: "https://github.com/Quick/Nimble.git", .upToNextMajor(from: "9.0.0")), .package(url: "https://github.com/MyEtherWallet/bls-eth-swift.git", .exact("1.0.1")), .package(url: "https://github.com/attaswift/BigInt.git", from: "5.2.1"), - .package(name: "MEWwalletTweetNacl", url: "https://github.com/MyEtherWallet/mew-wallet-ios-tweetnacl.git", .upToNextMajor(from: "1.0.0")) +// .package(name: "MEWwalletTweetNacl", url: "https://github.com/MyEtherWallet/mew-wallet-ios-tweetnacl.git", .upToNextMajor(from: "1.0.0")), + .package(name: "MEWwalletTweetNacl", url: "https://github.com/MoonfishApp/mew-wallet-ios-tweetnacl.git", .branch("main")), +// .package(name: "MEWwalletTweetNacl", path: "../mew-wallet-ios-tweetnacl"), ], targets: [ .target( diff --git a/Sources/Extensions/BLS/blsPublicKey+Data.swift b/Sources/Extensions/BLS/blsPublicKey+Data.swift index cda5fb5..9fcca82 100644 --- a/Sources/Extensions/BLS/blsPublicKey+Data.swift +++ b/Sources/Extensions/BLS/blsPublicKey+Data.swift @@ -6,6 +6,7 @@ // Copyright © 2020 MyEtherWallet Inc. All rights reserved. // + import Foundation import bls_framework diff --git a/Sources/eips/eip1024/EthEncryptedData+EIP1024.swift b/Sources/eips/eip1024/EthEncryptedData+EIP1024.swift index d6d165c..09f005b 100644 --- a/Sources/eips/eip1024/EthEncryptedData+EIP1024.swift +++ b/Sources/eips/eip1024/EthEncryptedData+EIP1024.swift @@ -14,6 +14,7 @@ public struct EthEncryptedData: Codable { public let nonce: String public let ephemPublicKey: String public let ciphertext: String + public private (set) var version = "x25519-xsalsa20-poly1305" public init(nonce: String, ephemPublicKey: String, ciphertext: String) { self.nonce = nonce @@ -23,40 +24,141 @@ public struct EthEncryptedData: Codable { } public enum EthCryptoError: Error { - case decryptionFailed + case decryptionFailed + case encryptionFailed + case keyError +} + +extension EthCryptoError: LocalizedError { + public var errorDescription: String? { + switch self { + case .decryptionFailed: + return "Decryption failed" + case .encryptionFailed: + return "Encryption failed" + case .keyError: + return "Invalid private key" + } + } } extension EthEncryptedData { - public func decrypt(privateKey: String) throws -> String { - let data = Data(hex: privateKey) + + // MARK: - Encrypt + /// - Parameters: + /// - plaintext: plain text to be encrypted + /// - senderKey: private ethereum key of sender + /// - publicKey: public key of recipient curve25519 in hex format + /// - Returns: EthEncryptedData + public static func encrypt(plaintext: String, senderPrivateKey: PrivateKeyEth1, recipientPublicKey: String) throws -> EthEncryptedData { + guard let plaintextData = plaintext.data(using: .utf8), + let recipientPublicKey = Data(base64Encoded: recipientPublicKey) else { + throw EthCryptoError.encryptionFailed + } + let secretBox = try TweetNacl.box(message: plaintextData, recipientPublicKey: recipientPublicKey, senderSecretKey: senderPrivateKey.curve25519PrivateKeyData()) + return try EthEncryptedData(nonce: secretBox.nonce.base64EncodedString(), ephemPublicKey: senderPrivateKey.curve25519PublicKey(), ciphertext: secretBox.box.base64EncodedString()) + } + + // MARK: - Decrypt + /// Decrypts EthEncryptedData + /// - Parameter privateKey: Private Ethereum key + /// - Returns: cleartext message + public func decrypt(privateKey: PrivateKeyEth1) throws -> String { + guard let privateKey = privateKey.string() else { throw EthCryptoError.keyError } + return try decrypt(privateKey: privateKey) + } + + /// Decrypts EthEncryptedData + /// - Parameter privateKey: String of private Ethereum key + /// - Returns: cleartext message + public func decrypt(privateKey: String) throws -> String { + let data = Data(hex: privateKey) - let secretKey = try TweetNacl.keyPair(fromSecretKey: data).secretKey - - guard let nonce = Data(base64Encoded: self.nonce), - let cipherText = Data(base64Encoded: self.ciphertext), - let ephemPublicKey = Data(base64Encoded: self.ephemPublicKey) else { - throw EthCryptoError.decryptionFailed - } - - let decrypted = try TweetNacl.open( - message: cipherText, - nonce: nonce, - publicKey: ephemPublicKey, - secretKey: secretKey - ) - - guard let message = String(data: decrypted, encoding: .utf8) else { - throw EthCryptoError.decryptionFailed - } - - return message + let secretKey = try TweetNacl.keyPair(fromSecretKey: data).secretKey + + guard let nonce = Data(base64Encoded: self.nonce), + let cipherText = Data(base64Encoded: self.ciphertext), + let ephemPublicKey = Data(base64Encoded: self.ephemPublicKey) else { + throw EthCryptoError.decryptionFailed + } + + let decrypted = try TweetNacl.open( + message: cipherText, + nonce: nonce, + publicKey: ephemPublicKey, + secretKey: secretKey + ) + + guard let message = String(data: decrypted, encoding: .utf8) else { + throw EthCryptoError.decryptionFailed + } + + return message + } + + /// Decrypt the message using the sender's private key + /// - Parameters: + /// - senderPrivateKey: sender private key as PrivateKeyEth1 + /// - recipientPublicKey: string of recipient's public key + /// - Returns: cleartext message + public func decrypt(senderPrivateKey: PrivateKeyEth1, recipientPublicKey: String) throws -> String { + guard let privateKey = senderPrivateKey.string() else { throw EthCryptoError.keyError } + return try decrypt(senderPrivateKey: privateKey, recipientPublicKey: recipientPublicKey) + } + + /// Decrypt the message using the sender's private key + /// - Parameters: + /// - senderPrivateKey: sender private key as string + /// - recipientPublicKey: string of recipient's public key + /// - Returns: cleartext message + public func decrypt(senderPrivateKey: String, recipientPublicKey: String) throws -> String { + let data = Data(hex: senderPrivateKey) + let secretKey = try TweetNacl.keyPair(fromSecretKey: data).secretKey + + guard let nonce = Data(base64Encoded: self.nonce), + let cipherText = Data(base64Encoded: self.ciphertext), + let recipientPublicKeyData = Data(base64Encoded: recipientPublicKey) else { + throw EthCryptoError.decryptionFailed + } + + let decrypted = try TweetNacl.open( + message: cipherText, + nonce: nonce, + publicKey: recipientPublicKeyData, + secretKey: secretKey + ) + + guard let message = String(data: decrypted, encoding: .utf8) else { + throw EthCryptoError.decryptionFailed } + + return message + } } extension PrivateKeyEth1 { - public func eth_publicKey() throws -> String { - let publicKey = try TweetNacl.keyPair(fromSecretKey: data()).publicKey - return publicKey.toHexString() + /// Create curve25519 public key from Ethereum private key + /// - Returns: base64 encoded string + public func curve25519PublicKey() throws -> String { + return try curve25519PublicKeyData().base64EncodedString() + } + + /// Create curve25519 public key from Ethereum private key + /// - Returns: public key + public func curve25519PublicKeyData() throws -> Data { + return try TweetNacl.keyPair(fromSecretKey: self.data()).publicKey + } + + /// Create curve25519 private key from Ethereum private key + /// - Returns: base64 encoded string + public func curve25519PrivateKey() throws -> String { + return try curve25519PrivateKeyData().base64EncodedString() + } + + /// Create curve25519 private key from Ethereum private key + /// - Returns: private key + public func curve25519PrivateKeyData() throws -> Data { + return try TweetNacl.keyPair(fromSecretKey: self.data()).secretKey } } diff --git a/Sources/eips/eip155/LegacyTransaction.swift b/Sources/eips/eip155/LegacyTransaction.swift index d082af9..ce1cf0c 100644 --- a/Sources/eips/eip155/LegacyTransaction.swift +++ b/Sources/eips/eip155/LegacyTransaction.swift @@ -14,7 +14,7 @@ public class LegacyTransaction: Transaction { return self._gasPrice.data } - init( + public init( nonce: BigInt = BigInt(0x00), gasPrice: BigInt = BigInt(0x00), gasLimit: BigInt = BigInt(0x00),