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

Add EIP1024 conform encryption #7

Draft
wants to merge 12 commits into
base: develop
Choose a base branch
from
56 changes: 55 additions & 1 deletion MEWwalletKitTests/Sources/eips/eip1024/EIP1024Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import Foundation
import Quick
import Nimble
import MEWwalletTweetNacl
@testable import MEWwalletKit

class EIP1024Tests: QuickSpec {
Expand All @@ -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)
}
}
}
}
}
5 changes: 3 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -22,7 +22,8 @@ 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"))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ronaldmannak can be reverted

//.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"),
],
targets: [
.target(
Expand Down
110 changes: 107 additions & 3 deletions Sources/eips/eip1024/EthEncryptedData+EIP1024.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -24,9 +25,52 @@ public struct EthEncryptedData: Codable {

public enum EthCryptoError: Error {
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 {

// 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)

Expand All @@ -51,12 +95,72 @@ extension EthEncryptedData {

return message
}


/// Decrypt the message using the sender's private key
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ronaldmannak this looks good to me, but indentations needs to be fixed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done and done

/// - 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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ronaldmannak can you please keep this method to not break back compatibility, but mark it as 'deprecated'?

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
Copy link
Collaborator

@Foboz Foboz Sep 2, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ronaldmannak in that case I think it's better to have private var curve25519Keypair and work with it in all 4 methods (actually it's better to use public var <name>: <type> { get throws { ... } }), instead of having 2 methods with calls of TweetNacl.keyPair dependencies on each other.

}

/// 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
}
}