From 31c2e26e91f17288d9d691a16d5ddebb34da3b66 Mon Sep 17 00:00:00 2001 From: Evan Kaloudis Date: Wed, 15 Apr 2026 17:15:38 -0400 Subject: [PATCH] fix: iOS: ITMS-90338: Replace non-public CCCryptorGCMOneshot APIs with CryptoKit --- ios/CryptoHelper.swift | 98 ++++++++++++++++++++ ios/LndMobile/Lnd.swift | 2 + ios/ZipUtils.m | 143 ++--------------------------- ios/zeus.xcodeproj/project.pbxproj | 4 + 4 files changed, 113 insertions(+), 134 deletions(-) create mode 100644 ios/CryptoHelper.swift diff --git a/ios/CryptoHelper.swift b/ios/CryptoHelper.swift new file mode 100644 index 000000000..6dc24588b --- /dev/null +++ b/ios/CryptoHelper.swift @@ -0,0 +1,98 @@ +import Foundation +import CryptoKit +import CommonCrypto + +@objc class CryptoHelper: NSObject { + + private static let cryptoVersion: UInt8 = 0x01 + private static let saltLen = 16 + private static let ivLen = 12 + private static let gcmTagLen = 16 + private static let pbkdf2Iterations: UInt32 = 100_000 + private static let keyLen = 32 // AES-256 + + @objc static func encryptData(_ plaintext: Data, passphrase: String) throws -> Data { + var salt = Data(count: saltLen) + var iv = Data(count: ivLen) + try salt.withUnsafeMutableBytes { buf in + guard SecRandomCopyBytes(kSecRandomDefault, saltLen, buf.baseAddress!) == errSecSuccess else { + throw NSError(domain: "CryptoHelper", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to generate random salt"]) + } + } + try iv.withUnsafeMutableBytes { buf in + guard SecRandomCopyBytes(kSecRandomDefault, ivLen, buf.baseAddress!) == errSecSuccess else { + throw NSError(domain: "CryptoHelper", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to generate random IV"]) + } + } + + let derivedKey = try deriveKey(passphrase: passphrase, salt: salt) + let symmetricKey = SymmetricKey(data: derivedKey) + let nonce = try AES.GCM.Nonce(data: iv) + let sealedBox = try AES.GCM.seal(plaintext, using: symmetricKey, nonce: nonce) + + // Wire format: [version][salt][iv][ciphertext][tag] + var output = Data() + output.append(cryptoVersion) + output.append(salt) + output.append(iv) + output.append(sealedBox.ciphertext) + output.append(sealedBox.tag) + return output + } + + @objc static func decryptData(_ fileData: Data, passphrase: String) throws -> Data { + let minLen = 1 + saltLen + ivLen + gcmTagLen + guard fileData.count >= minLen else { + throw NSError(domain: "CryptoHelper", code: -2, userInfo: [NSLocalizedDescriptionKey: "File too small to be a valid encrypted backup"]) + } + + let version = fileData[fileData.startIndex] + guard version == cryptoVersion else { + throw NSError(domain: "CryptoHelper", code: -3, userInfo: [NSLocalizedDescriptionKey: "Unsupported encryption version: \(version)"]) + } + + let saltStart = fileData.startIndex + 1 + let ivStart = saltStart + saltLen + let ciphertextStart = ivStart + ivLen + let tagStart = fileData.endIndex - gcmTagLen + + let salt = fileData[saltStart.. Data { + guard let passphraseData = passphrase.data(using: .utf8) else { + throw NSError(domain: "CryptoHelper", code: -4, userInfo: [NSLocalizedDescriptionKey: "Failed to encode passphrase"]) + } + var derivedKey = Data(count: keyLen) + let status = derivedKey.withUnsafeMutableBytes { keyBuf in + salt.withUnsafeBytes { saltBuf in + passphraseData.withUnsafeBytes { passBuf in + CCKeyDerivationPBKDF( + CCPBKDFAlgorithm(kCCPBKDF2), + passBuf.baseAddress?.assumingMemoryBound(to: Int8.self), + passphraseData.count, + saltBuf.baseAddress?.assumingMemoryBound(to: UInt8.self), + saltLen, + CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256), + pbkdf2Iterations, + keyBuf.baseAddress?.assumingMemoryBound(to: UInt8.self), + keyLen + ) + } + } + } + guard status == kCCSuccess else { + throw NSError(domain: "CryptoHelper", code: -5, userInfo: [NSLocalizedDescriptionKey: "Key derivation failed"]) + } + return derivedKey + } +} diff --git a/ios/LndMobile/Lnd.swift b/ios/LndMobile/Lnd.swift index d3873a58f..f8973baf1 100644 --- a/ios/LndMobile/Lnd.swift +++ b/ios/LndMobile/Lnd.swift @@ -23,6 +23,8 @@ typealias Callback = (Data?, Error?) -> Void typealias StreamCallback = (Data?, Error?) -> Void // Used internally in this class to deal with Lndmobile/Go +// Renamed in ObjC to avoid collision with Lndmobile.xcframework's own LndmobileCallback +@objc(ZeusLndmobileCallback) class LndmobileCallback: NSObject, LndmobileCallbackProtocol { var method: String var callback: Callback diff --git a/ios/ZipUtils.m b/ios/ZipUtils.m index 43479d187..5d9ac4fc7 100644 --- a/ios/ZipUtils.m +++ b/ios/ZipUtils.m @@ -1,28 +1,6 @@ #import "ZipUtils.h" #import -#import -#import - -// These GCM oneshot functions are available since iOS 13 but are not -// declared in the public CommonCrypto headers. Provide explicit -// prototypes so the build succeeds under -Wimplicit-function-declaration. -CCCryptorStatus CCCryptorGCMOneshotEncrypt( - CCAlgorithm alg, const void *key, size_t keyLength, - const void *iv, size_t ivLen, - const void *aData, size_t aDataLen, - const void *dataIn, size_t dataInLength, - void *dataOut, - void *tagOut, size_t tagLength -) __attribute__((weak_import)); - -CCCryptorStatus CCCryptorGCMOneshotDecrypt( - CCAlgorithm alg, const void *key, size_t keyLength, - const void *iv, size_t ivLen, - const void *aData, size_t aDataLen, - const void *dataIn, size_t dataInLength, - void *dataOut, - const void *tagIn, size_t tagLength -) __attribute__((weak_import)); +#import "zeus-Swift.h" // Minizip-compatible local file header constants #define ZIP_LOCAL_HEADER_SIGNATURE 0x04034b50 @@ -323,15 +301,6 @@ RCT_EXPORT_METHOD(unzipFile:(NSString *)zipPath return decompressed; } -#pragma mark - Encryption constants - -static const uint8_t CRYPTO_VERSION = 0x01; -static const size_t SALT_LEN = 16; -static const size_t IV_LEN = 12; -static const size_t GCM_TAG_LEN = 16; -static const uint32_t PBKDF2_ITERATIONS = 100000; -static const size_t KEY_LEN = 32; // AES-256 - #pragma mark - Encrypt RCT_EXPORT_METHOD(encryptFile:(NSString *)inputPath @@ -347,59 +316,13 @@ RCT_EXPORT_METHOD(encryptFile:(NSString *)inputPath return; } - // Generate random salt and IV - uint8_t salt[SALT_LEN]; - uint8_t iv[IV_LEN]; - if (SecRandomCopyBytes(kSecRandomDefault, SALT_LEN, salt) != errSecSuccess || - SecRandomCopyBytes(kSecRandomDefault, IV_LEN, iv) != errSecSuccess) { - reject(@"ERR_ENCRYPT", @"Failed to generate random bytes", nil); + NSError *error = nil; + NSData *output = [CryptoHelper encryptData:plaintext passphrase:passphrase error:&error]; + if (!output) { + reject(@"ERR_ENCRYPT", error.localizedDescription ?: @"Encryption failed", error); return; } - // Derive key with PBKDF2-SHA256 - NSData *passphraseData = [passphrase dataUsingEncoding:NSUTF8StringEncoding]; - uint8_t derivedKey[KEY_LEN]; - CCStatus kdfStatus = CCKeyDerivationPBKDF( - kCCPBKDF2, - passphraseData.bytes, passphraseData.length, - salt, SALT_LEN, - kCCPRFHmacAlgSHA256, - PBKDF2_ITERATIONS, - derivedKey, KEY_LEN - ); - if (kdfStatus != kCCSuccess) { - reject(@"ERR_ENCRYPT", @"Key derivation failed", nil); - return; - } - - // AES-256-GCM encrypt - size_t ciphertextLen = plaintext.length; - NSMutableData *ciphertext = [NSMutableData dataWithLength:ciphertextLen]; - uint8_t tag[GCM_TAG_LEN]; - - CCCryptorStatus status = CCCryptorGCMOneshotEncrypt( - kCCAlgorithmAES, - derivedKey, KEY_LEN, - iv, IV_LEN, - NULL, 0, // no AAD - plaintext.bytes, plaintext.length, - ciphertext.mutableBytes, - tag, GCM_TAG_LEN - ); - - if (status != kCCSuccess) { - reject(@"ERR_ENCRYPT", [NSString stringWithFormat:@"Encryption failed with status: %d", status], nil); - return; - } - - // Write: [version][salt][iv][ciphertext][tag] - NSMutableData *output = [NSMutableData data]; - [output appendBytes:&CRYPTO_VERSION length:1]; - [output appendBytes:salt length:SALT_LEN]; - [output appendBytes:iv length:IV_LEN]; - [output appendData:ciphertext]; - [output appendBytes:tag length:GCM_TAG_LEN]; - if ([output writeToFile:outputPath atomically:YES]) { resolve(nil); } else { @@ -423,58 +346,10 @@ RCT_EXPORT_METHOD(decryptFile:(NSString *)inputPath return; } - size_t minLen = 1 + SALT_LEN + IV_LEN + GCM_TAG_LEN; - if (fileData.length < minLen) { - reject(@"ERR_DECRYPT", @"File too small to be a valid encrypted backup", nil); - return; - } - - const uint8_t *bytes = fileData.bytes; - - uint8_t version = bytes[0]; - if (version != CRYPTO_VERSION) { - reject(@"ERR_DECRYPT", [NSString stringWithFormat:@"Unsupported encryption version: %d", version], nil); - return; - } - - const uint8_t *salt = bytes + 1; - const uint8_t *iv = bytes + 1 + SALT_LEN; - size_t headerLen = 1 + SALT_LEN + IV_LEN; - size_t ciphertextLen = fileData.length - headerLen - GCM_TAG_LEN; - const uint8_t *ciphertextBytes = bytes + headerLen; - const uint8_t *tagBytes = bytes + headerLen + ciphertextLen; - - // Derive key with PBKDF2-SHA256 - NSData *passphraseData = [passphrase dataUsingEncoding:NSUTF8StringEncoding]; - uint8_t derivedKey[KEY_LEN]; - CCStatus kdfStatus = CCKeyDerivationPBKDF( - kCCPBKDF2, - passphraseData.bytes, passphraseData.length, - salt, SALT_LEN, - kCCPRFHmacAlgSHA256, - PBKDF2_ITERATIONS, - derivedKey, KEY_LEN - ); - if (kdfStatus != kCCSuccess) { - reject(@"ERR_DECRYPT", @"Key derivation failed", nil); - return; - } - - // AES-256-GCM decrypt - NSMutableData *plaintext = [NSMutableData dataWithLength:ciphertextLen]; - - CCCryptorStatus status = CCCryptorGCMOneshotDecrypt( - kCCAlgorithmAES, - derivedKey, KEY_LEN, - iv, IV_LEN, - NULL, 0, // no AAD - ciphertextBytes, ciphertextLen, - plaintext.mutableBytes, - tagBytes, GCM_TAG_LEN - ); - - if (status != kCCSuccess) { - reject(@"ERR_DECRYPT", @"Decryption failed. Incorrect seed or corrupted file.", nil); + NSError *error = nil; + NSData *plaintext = [CryptoHelper decryptData:fileData passphrase:passphrase error:&error]; + if (!plaintext) { + reject(@"ERR_DECRYPT", error.localizedDescription ?: @"Decryption failed. Incorrect seed or corrupted file.", error); return; } diff --git a/ios/zeus.xcodeproj/project.pbxproj b/ios/zeus.xcodeproj/project.pbxproj index 0d484e6a9..c0c09db49 100644 --- a/ios/zeus.xcodeproj/project.pbxproj +++ b/ios/zeus.xcodeproj/project.pbxproj @@ -66,6 +66,7 @@ 4A9A9C2189D54ED78D4029E2 /* Wrench.svg in Resources */ = {isa = PBXBuildFile; fileRef = C18B7331C6704C59A8BB7DCA /* Wrench.svg */; }; 4F7B0DD5ACBD432DB04374DC /* Speedometer.svg in Resources */ = {isa = PBXBuildFile; fileRef = 4B58C327668640C881703E94 /* Speedometer.svg */; }; 5100FA362F75348D009D640A /* ZipUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 5100FA352F75348D009D640A /* ZipUtils.m */; }; + 5100FA382F75348D009D640A /* CryptoHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5100FA372F75348D009D640A /* CryptoHelper.swift */; }; 5134B5512EB7C1B000A6AD03 /* ShareQR.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 5134B5472EB7C1B000A6AD03 /* ShareQR.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 51BF29FE2ED5FC770044B4FB /* ShareBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 51BF29FC2ED5FB940044B4FB /* ShareBridge.m */; }; 541A24C3CF314FAF9DAFEE06 /* Arrow_down.svg in Resources */ = {isa = PBXBuildFile; fileRef = 16305D1EA8FA48288C57F6A7 /* Arrow_down.svg */; }; @@ -513,6 +514,7 @@ 5014345A129344348443D20F /* zeus-pay.svg */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "zeus-pay.svg"; path = "../assets/images/SVG/zeus-pay.svg"; sourceTree = ""; }; 5100FA342F75348D009D640A /* ZipUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ZipUtils.h; sourceTree = ""; }; 5100FA352F75348D009D640A /* ZipUtils.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ZipUtils.m; sourceTree = ""; }; + 5100FA372F75348D009D640A /* CryptoHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoHelper.swift; sourceTree = ""; }; 51184C7F24744DCB90D53B8C /* wordmark-black.svg */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "wordmark-black.svg"; path = "../assets/images/SVG/wordmark-black.svg"; sourceTree = ""; }; 5134B5472EB7C1B000A6AD03 /* ShareQR.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareQR.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 51BF29FB2ED5FB940044B4FB /* ShareBridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ShareBridge.h; sourceTree = ""; }; @@ -849,6 +851,7 @@ children = ( 5100FA342F75348D009D640A /* ZipUtils.h */, 5100FA352F75348D009D640A /* ZipUtils.m */, + 5100FA372F75348D009D640A /* CryptoHelper.swift */, 51BF29FB2ED5FB940044B4FB /* ShareBridge.h */, 51BF29FC2ED5FB940044B4FB /* ShareBridge.m */, B1C2D3E42D36C99900000001 /* LdkNodeMobile */, @@ -1963,6 +1966,7 @@ 745388492A54C2B6000927CE /* walletunlocker.pb.swift in Sources */, 7453884A2A54C2B6000927CE /* LndMobileTools.m in Sources */, 5100FA362F75348D009D640A /* ZipUtils.m in Sources */, + 5100FA382F75348D009D640A /* CryptoHelper.swift in Sources */, 745388472A54C2B6000927CE /* LndMobileScheduledSync.m in Sources */, 745388462A54C2B6000927CE /* LndMobile.m in Sources */, 7453884B2A54C2B6000927CE /* Lnd.swift in Sources */,