Skip to content

Commit

Permalink
Merge branch 'feature/comment_encrypt'
Browse files Browse the repository at this point in the history
  • Loading branch information
grishamsc committed Oct 10, 2024
2 parents 5f58cb5 + 0813b47 commit 1a7c917
Show file tree
Hide file tree
Showing 11 changed files with 407 additions and 1 deletion.
4 changes: 3 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ 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(
name: "TonSwift",
dependencies: [
.product(name: "BigInt", package: "BigInt"),
.product(name: "TweetNacl", package: "tweetnacl-swiftwrap"),
.product(name: "Sodium", package: "swift-sodium"),
]),
.testTarget(
name: "TonSwiftTests",
Expand Down
77 changes: 77 additions & 0 deletions Source/TonSwift/CommentEncryption/CommentDecryptor.swift
Original file line number Diff line number Diff line change
@@ -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..<publicKey.data.count])
let encryptedData = Array(cipherTextBytes[publicKey.data.count..<cipherTextBytes.count])
guard encryptedData.count >= 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..<encryptedData.count]))

let cbcStateSecret = HMAC_SHA512.hmacSha512(message: msgKey, key: sharedSecret)

let aesKey = cbcStateSecret[0..<32]
let iv = cbcStateSecret[32..<48]
let decryptedData = try AES_CBC(key: aesKey, iv: iv).decrypt(cipherData: data)

let salt = senderAddress.toFriendly(testOnly: false, bounceable: true).toString().data(using: .utf8) ?? Data()

let decryptedDataHash = HMAC_SHA512.hmacSha512(message: decryptedData, key: salt)

let encryptionMsgKey = decryptedDataHash[0..<16]
guard encryptionMsgKey == msgKey else {
throw Error.incorrectHash
}

let prefixLength = Int(decryptedData[0])
guard prefixLength <= decryptedData.count && prefixLength >= 16 else {
throw Error.incorrectPrefixSize
}

let decryptedMessage = decryptedData[prefixLength..<decryptedData.count]
return String(data: decryptedMessage, encoding: .utf8)
}
}
70 changes: 70 additions & 0 deletions Source/TonSwift/CommentEncryption/CommentEncryptor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import Foundation

public struct CommentEncryptor {
enum Error: Swift.Error {
case commentIsEmpty
case failedGetSharedSecret(error: Swift.Error)
case incorrectDataSize
}

private let comment: String
private let senderPublicKey: PublicKey
private let senderPrivateKey: PrivateKey
private let peerPublicKey: PublicKey
private let senderAddress: Address

public init(comment: String,
senderPublicKey: PublicKey,
senderPrivateKey: PrivateKey,
peerPublicKey: PublicKey,
senderAddress: Address) {
self.comment = comment
self.senderPublicKey = senderPublicKey
self.senderPrivateKey = senderPrivateKey
self.peerPublicKey = peerPublicKey
self.senderAddress = senderAddress
}

public func encrypt() throws -> 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
}
}
Original file line number Diff line number Diff line change
@@ -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()
}
}
17 changes: 17 additions & 0 deletions Source/TonSwift/Crypto/25519/Ed25519.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
84 changes: 84 additions & 0 deletions Source/TonSwift/Crypto/25519/X25519.swift
Original file line number Diff line number Diff line change
@@ -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<UInt8>(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<UInt8>(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<UInt8>(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
}
65 changes: 65 additions & 0 deletions Source/TonSwift/Crypto/AES_CBC.swift
Original file line number Diff line number Diff line change
@@ -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<UInt8>(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<UInt8>(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)
}
}
28 changes: 28 additions & 0 deletions Source/TonSwift/Crypto/HMAC_SHA512.swift
Original file line number Diff line number Diff line change
@@ -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<UInt8>(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)
}
}

1 change: 1 addition & 0 deletions Source/TonSwift/Util/OpCodes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Loading

0 comments on commit 1a7c917

Please sign in to comment.