fix: iOS: ITMS-90338: Replace non-public CCCryptorGCMOneshot APIs with CryptoKit

This commit is contained in:
Evan Kaloudis
2026-04-15 17:15:38 -04:00
parent c000d08c4a
commit 31c2e26e91
4 changed files with 113 additions and 134 deletions
+98
View File
@@ -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..<ivStart]
let iv = fileData[ivStart..<ciphertextStart]
let ciphertext = fileData[ciphertextStart..<tagStart]
let tag = fileData[tagStart..<fileData.endIndex]
let derivedKey = try deriveKey(passphrase: passphrase, salt: Data(salt))
let symmetricKey = SymmetricKey(data: derivedKey)
let nonce = try AES.GCM.Nonce(data: iv)
let sealedBox = try AES.GCM.SealedBox(nonce: nonce, ciphertext: ciphertext, tag: tag)
return try AES.GCM.open(sealedBox, using: symmetricKey)
}
private static func deriveKey(passphrase: String, salt: Data) throws -> 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
}
}
+2
View File
@@ -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
+9 -134
View File
@@ -1,28 +1,6 @@
#import "ZipUtils.h"
#import <zlib.h>
#import <CommonCrypto/CommonCryptor.h>
#import <CommonCrypto/CommonKeyDerivation.h>
// 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;
}
+4
View File
@@ -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 = "<group>"; };
5100FA342F75348D009D640A /* ZipUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ZipUtils.h; sourceTree = "<group>"; };
5100FA352F75348D009D640A /* ZipUtils.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ZipUtils.m; sourceTree = "<group>"; };
5100FA372F75348D009D640A /* CryptoHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoHelper.swift; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
@@ -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 */,