diff --git a/Package.swift b/Package.swift index 95cc3be..09e86e3 100644 --- a/Package.swift +++ b/Package.swift @@ -12,7 +12,8 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/attaswift/BigInt", .exact("5.3.0")), - .package(url: "https://github.com/bitmark-inc/tweetnacl-swiftwrap", .upToNextMajor(from: "1.0.0")) + .package(url: "https://github.com/bitmark-inc/tweetnacl-swiftwrap", .upToNextMajor(from: "1.0.0")), + .package(url: "https://github.com/jedisct1/swift-sodium", .exact("0.9.1")) ], targets: [ .target( @@ -20,6 +21,7 @@ let package = Package( dependencies: [ .product(name: "BigInt", package: "BigInt"), .product(name: "TweetNacl", package: "tweetnacl-swiftwrap"), + .product(name: "Sodium", package: "swift-sodium"), ]), .testTarget( name: "TonSwiftTests", diff --git a/Source/TonSwift/CommentEncryption/CommentDecryptor.swift b/Source/TonSwift/CommentEncryption/CommentDecryptor.swift new file mode 100644 index 0000000..8abebae --- /dev/null +++ b/Source/TonSwift/CommentEncryption/CommentDecryptor.swift @@ -0,0 +1,77 @@ +import Foundation + +public struct CommentDecryptor { + enum Error: Swift.Error { + case incorrectCipherData + case incorrectCipherDataLength + case failedGetSharedSecret(error: Swift.Error) + case incorrectEncryptedDataSize + case incorrectHash + case incorrectPrefixSize + } + + private let privateKey: PrivateKey + private let publicKey: PublicKey + private let cipherText: String + private let senderAddress: Address + + public init(privateKey: PrivateKey, + publicKey: PublicKey, + cipherText: String, + senderAddress: Address) { + self.privateKey = privateKey + self.publicKey = publicKey + self.cipherText = cipherText + self.senderAddress = senderAddress + } + + public func decrypt() throws -> String? { + guard let cipherTextData = Data(hex: cipherText) else { + throw Error.incorrectCipherData + } + + let cipherTextBytes = [UInt8](cipherTextData) + guard cipherTextBytes.count >= publicKey.data.count else { + throw Error.incorrectCipherDataLength + } + let cipherTextPublicKeyBytes = Array(cipherTextBytes[0..= 16, encryptedData.count % 16 == 0 else { + throw Error.incorrectEncryptedDataSize + } + + let senderPublicKey = PublicKey(data: Data([UInt8](publicKey.data).enumerated().map { $0.element ^ cipherTextPublicKeyBytes[$0.offset] })) + let sharedSecret: Data + do { + sharedSecret = try Ed25519.getSharedSecret(privateKey: privateKey, publicKey: senderPublicKey) + } catch { + throw Error.failedGetSharedSecret(error: error) + } + + let msgKey = Data(Array(encryptedData[0..<16])) + let data = Data(Array(encryptedData[16..= 16 else { + throw Error.incorrectPrefixSize + } + + let decryptedMessage = decryptedData[prefixLength.. Data { + guard !comment.isEmpty else { + throw Error.commentIsEmpty + } + + let commentData = comment.data(using: .utf8) ?? Data() + let salt = senderAddress.toFriendly(testOnly: false, bounceable: true).toString().data(using: .utf8) ?? Data() + + let sharedSecret: Data + do { + sharedSecret = try Ed25519.getSharedSecret(privateKey: senderPrivateKey, publicKey: peerPublicKey) + } catch { + throw Error.failedGetSharedSecret(error: error) + } + + let prefix = try getRandomPrefix(dataLength: commentData.count, minPadding: 16) + let data = prefix + commentData + + guard data.count % 16 == 0 else { throw Error.incorrectDataSize } + + let dataHash = HMAC_SHA512.hmacSha512(message: data, key: salt) + let msgKey = dataHash[0..<16] + let cbcStateSecret = HMAC_SHA512.hmacSha512(message: msgKey, key: sharedSecret) + + let aesKey = cbcStateSecret[0..<32] + let iv = cbcStateSecret[32..<48] + let encrypted = try AES_CBC(key: aesKey, iv: iv).encrypt(data: data) + + let encryptedData = msgKey + encrypted + + let cipherTextPrefix = peerPublicKey.data.enumerated().map { $0.element ^ senderPublicKey.data[$0.offset] } + let cipherTextData = Data(cipherTextPrefix + encryptedData) + + return cipherTextData + } + + private func getRandomPrefix(dataLength: Int, minPadding: Int) throws -> Data { + let prefixLength = ((minPadding + 15 + dataLength) & -16) - dataLength + var prefix = try RandomBytes.generate(length: prefixLength) + prefix[0] = withUnsafeBytes(of: prefixLength.littleEndian, Array.init)[0] + return prefix + } +} diff --git a/Source/TonSwift/CommentEncryption/EncryptedCommentCellBuilder.swift b/Source/TonSwift/CommentEncryption/EncryptedCommentCellBuilder.swift new file mode 100644 index 0000000..a600d35 --- /dev/null +++ b/Source/TonSwift/CommentEncryption/EncryptedCommentCellBuilder.swift @@ -0,0 +1,11 @@ +import Foundation + +public struct EncryptedCommentCellBuilder { + public static func buildCell(encryptedData: Data) throws -> Cell { + let opCodeData = Data(withUnsafeBytes(of: OpCodes.ENCRYPTED_COMMENT.bigEndian, Array.init)) + let payloadData = opCodeData + encryptedData + + let builder = Builder() + return try builder.writeSnakeData(payloadData).endCell() + } +} diff --git a/Source/TonSwift/Crypto/25519/Ed25519.swift b/Source/TonSwift/Crypto/25519/Ed25519.swift new file mode 100644 index 0000000..31a27dc --- /dev/null +++ b/Source/TonSwift/Crypto/25519/Ed25519.swift @@ -0,0 +1,17 @@ +import Foundation + +public enum Ed25519 { + public enum Error: Swift.Error { + case sharedSecretError(Swift.Error) + } + + public static func getSharedSecret(privateKey: PrivateKey, publicKey: PublicKey) throws -> Data { + do { + let xPrivateKey = try privateKey.toX25519 + let xPublicKey = try publicKey.toX25519 + return try X25519.getSharedSecret(privateKey: xPrivateKey, publicKey: xPublicKey) + } catch { + throw Error.sharedSecretError(error) + } + } +} diff --git a/Source/TonSwift/Crypto/25519/X25519.swift b/Source/TonSwift/Crypto/25519/X25519.swift new file mode 100644 index 0000000..6f861e9 --- /dev/null +++ b/Source/TonSwift/Crypto/25519/X25519.swift @@ -0,0 +1,84 @@ +import Foundation +import Clibsodium + +public enum X25519 { + enum Error: Swift.Error { + case sharedSecretError(code: Int) + } + + public struct PrivateKey: Key, Equatable, Codable { + public let data: Data + + public init(data: Data) { + self.data = data + } + } + + public struct PublicKey: Key, Equatable, Codable { + public let data: Data + + public init(data: Data) { + self.data = data + } + } + + static func getSharedSecret(privateKey: X25519.PrivateKey, publicKey: X25519.PublicKey) throws -> Data { + var outputBuffer = Array(repeating: 0, count: .sharedSecretLength) + try privateKey.data.withUnsafeBytes { bufferPointer in + guard let privateKeyPointer = bufferPointer.baseAddress else { return } + try publicKey.data.withUnsafeBytes { bufferPointer in + guard let publicKeyPointer = bufferPointer.baseAddress else { return } + let statusCode = crypto_scalarmult(&outputBuffer, privateKeyPointer, publicKeyPointer) + guard statusCode == 0 else { + throw Error.sharedSecretError(code: Int(statusCode)) + } + } + } + return Data(outputBuffer) + } +} + +public extension X25519 { + enum X25519ConversionError: Swift.Error { + case publicKeyConversionFailed(code: Int) + case privateKeyConversionFailed(code: Int) + } +} + +extension PublicKey { + var toX25519: X25519.PublicKey { + get throws { + var outputBuffer = Array(repeating: 0, count: .publicKeyLength) + try data.withUnsafeBytes { buffer in + guard let pointer = buffer.baseAddress else { return } + let statusCode = crypto_sign_ed25519_pk_to_curve25519(&outputBuffer, pointer) + guard statusCode == 0 else { + throw X25519.X25519ConversionError.publicKeyConversionFailed(code: Int(statusCode)) + } + } + return X25519.PublicKey(data: Data(outputBuffer)) + } + } +} + +extension PrivateKey { + var toX25519: X25519.PrivateKey { + get throws { + var outputBuffer = Array(repeating: 0, count: .privateKeyLength) + try data.withUnsafeBytes { buffer in + guard let pointer = buffer.baseAddress else { return } + let statusCode = crypto_sign_ed25519_sk_to_curve25519(&outputBuffer, pointer) + guard statusCode == 0 else { + throw X25519.X25519ConversionError.privateKeyConversionFailed(code: Int(statusCode)) + } + } + return X25519.PrivateKey(data: Data(outputBuffer)) + } + } +} + +private extension Int { + static let privateKeyLength: Int = 32 + static let publicKeyLength: Int = 32 + static let sharedSecretLength: Int = 32 +} diff --git a/Source/TonSwift/Crypto/AES_CBC.swift b/Source/TonSwift/Crypto/AES_CBC.swift new file mode 100644 index 0000000..27e19c1 --- /dev/null +++ b/Source/TonSwift/Crypto/AES_CBC.swift @@ -0,0 +1,65 @@ +import Foundation +import CommonCrypto + +public struct AES_CBC { + enum Error: Swift.Error { + case encryptionError(status: CCCryptorStatus) + case decryptionError(status: CCCryptorStatus) + } + + public let key: Data + public let iv: Data + + public init(key: Data, + iv: Data) { + self.key = key + self.iv = iv + } + + public func decrypt(cipherData: Data) throws -> Data { + var outputBuffer = Array(repeating: 0, count: cipherData.count + kCCBlockSizeAES128) + var numBytesDecrypted = 0 + + let status = CCCrypt(CCOperation(kCCDecrypt), + CCAlgorithm(kCCAlgorithmAES), + CCOptions(kCCOptionPKCS7Padding), + Array(key), + kCCKeySizeAES256, + Array(iv), + Array(cipherData), + cipherData.count, + &outputBuffer, + outputBuffer.count, + &numBytesDecrypted) + + guard status == kCCSuccess else { + throw Error.decryptionError(status: status) + } + + let outputBytes = outputBuffer.prefix(numBytesDecrypted) + return Data(outputBytes) + } + + public func encrypt(data: Data) throws -> Data { + var outputBuffer = Array(repeating: 0, count: data.count + kCCBlockSizeAES128) + var numBytesEncrypted = 0 + + let status = CCCrypt(CCOperation(kCCEncrypt), + CCAlgorithm(kCCAlgorithmAES), + CCOptions(kCCOptionPKCS7Padding), + Array(key), + kCCKeySizeAES256, + Array(iv), + Array(data), + data.count, + &outputBuffer, + outputBuffer.count, + &numBytesEncrypted) + + guard status == kCCSuccess else { + throw Error.encryptionError(status: status) + } + let outputBytes = outputBuffer.prefix(numBytesEncrypted) + return Data(outputBytes) + } +} diff --git a/Source/TonSwift/Crypto/HMAC_SHA512.swift b/Source/TonSwift/Crypto/HMAC_SHA512.swift new file mode 100644 index 0000000..08db66b --- /dev/null +++ b/Source/TonSwift/Crypto/HMAC_SHA512.swift @@ -0,0 +1,28 @@ +import Foundation +import CommonCrypto + +public struct HMAC_SHA512 { + public static func hmacSha512(message: Data, key: Data) -> Data { + let count = Int(CC_SHA512_DIGEST_LENGTH) + var outputBuffer = Array(repeating: 0, count: count) + + key.withUnsafeBytes { bufferPointer in + guard let keyPointer = bufferPointer.baseAddress else { return } + message.withUnsafeBytes { bufferPointer in + guard let messagePointer = bufferPointer.baseAddress else { return } + CCHmac( + CCHmacAlgorithm( + kCCHmacAlgSHA512 + ), + keyPointer, + key.count, + messagePointer, + message.count, + &outputBuffer + ) + } + } + return Data(bytes: outputBuffer, count: count) + } +} + diff --git a/Source/TonSwift/Util/OpCodes.swift b/Source/TonSwift/Util/OpCodes.swift index a911a71..efb0b54 100644 --- a/Source/TonSwift/Util/OpCodes.swift +++ b/Source/TonSwift/Util/OpCodes.swift @@ -10,4 +10,5 @@ public enum OpCodes { public static var LIQUID_TF_BURN: Int32 = 0x595f07bc public static var WHALES_DEPOSIT: Int32 = 2077040623 public static var WHALES_WITHDRAW: UInt32 = 3665837821 + public static var ENCRYPTED_COMMENT: Int32 = 0x2167da4b } diff --git a/Source/TonSwift/Util/RandomBytes.swift b/Source/TonSwift/Util/RandomBytes.swift new file mode 100644 index 0000000..b2cb2cb --- /dev/null +++ b/Source/TonSwift/Util/RandomBytes.swift @@ -0,0 +1,20 @@ +import Foundation + +public struct RandomBytes { + public enum Error: Swift.Error { + case failedGenerate(statusCode: Int) + case other + } + + public static func generate(length: Int) throws -> Data { + var outputBuffer = Data(count: length) + let resultCode = try outputBuffer.withUnsafeMutableBytes { + guard let baseAddress = $0.baseAddress else { throw Error.other } + return SecRandomCopyBytes(kSecRandomDefault, length, baseAddress) + } + guard resultCode == errSecSuccess else { + throw Error.failedGenerate(statusCode: Int(resultCode)) + } + return outputBuffer + } +} diff --git a/Tests/TonSwiftTests/CommentEncryption/CommentDecryptorTests.swift b/Tests/TonSwiftTests/CommentEncryption/CommentDecryptorTests.swift new file mode 100644 index 0000000..85fc06f --- /dev/null +++ b/Tests/TonSwiftTests/CommentEncryption/CommentDecryptorTests.swift @@ -0,0 +1,31 @@ +import XCTest +@testable import TonSwift + +final class CommentEncryptionTests: XCTestCase { + func testA() throws { + let mnemonicA = Mnemonic.mnemonicNew() + let keyPairA = try Mnemonic.mnemonicToPrivateKey(mnemonicArray: mnemonicA) + + let mnemonicB = Mnemonic.mnemonicNew() + let keyPairB = try Mnemonic.mnemonicToPrivateKey(mnemonicArray: mnemonicB) + + let message = "this is the best message in the world" + + let encrypted = try CommentEncryptor( + comment: message, + senderPublicKey: keyPairA.publicKey, + senderPrivateKey: keyPairA.privateKey, + peerPublicKey: keyPairB.publicKey, + senderAddress: try Address.parse("0:25603f6d7d3a1c6f981f02237c917150a6f2af971e83dd9e19605fa57a5d5b00") + ).encrypt() + + let decrypted = try CommentDecryptor( + privateKey: keyPairB.privateKey, + publicKey: keyPairB.publicKey, + cipherText: encrypted.hexString(), + senderAddress: try Address.parse("0:25603f6d7d3a1c6f981f02237c917150a6f2af971e83dd9e19605fa57a5d5b00") + ).decrypt() + + XCTAssertEqual(decrypted, message) + } +}