-
Notifications
You must be signed in to change notification settings - Fork 1
fix: prevent keychain persistence on app uninstall #299
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
Closed
Closed
Changes from 8 commits
Commits
Show all changes
28 commits
Select commit
Hold shift + click to select a range
d5c6690
feat: create KeychainCrypto
jvsena42 6cfa94d
chore: create failedToDecrypt error type
jvsena42 ae29e25
feat: integrate encryption to keychain operations
jvsena42 b358977
feat: handle orphaned keychain scenario
jvsena42 098bdec
chore: add keychain key to wipe method
jvsena42 8fa99f2
test: KeychainCryptoTests
jvsena42 3810ac5
test: update keychain tests
jvsena42 c0b61c0
fix: disable icloud data sync
jvsena42 ab645ca
fix: handle migration
jvsena42 9578348
fix: check if encryption key exists BEFORE attempting decryption
jvsena42 e47ce9a
chore: extract app group identifier
jvsena42 af84f11
chore: convert var to let
jvsena42 03f937b
fix: revert commit
jvsena42 6943c77
fix: improve orphaned and migration diferentiation
jvsena42 1ab0738
chore: remove unused attribute
jvsena42 b855dfb
Merge branch 'master' into fix/conflicts
jvsena42 e548bc3
fix: wipe RN keychain after migration
jvsena42 a6e94a3
fix: don't create another setup if one succeeded
jvsena42 751a505
fix: prevent backup triggering during migrations
jvsena42 f0dacd1
Merge branch 'master' into fix/clean-keychain-persistence-uninstall
jvsena42 3d3a28f
Merge branch 'fix/clean-keychain-persistence-uninstall' of github.com…
jvsena42 d2d11bf
Merge branch 'fix/pin-not-accepted' into fix/clean-keychain-persisten…
jvsena42 92920e0
fix: check for orphaned RN data before doing the migration
jvsena42 3000a78
fix: add conditional compilation to prevent BackupService being compi…
jvsena42 9a48db5
Merge branch 'master' into fix/clean-keychain-persistence-uninstall
jvsena42 613a97e
Merge branch 'master' into fix/clean-keychain-persistence-uninstall
jvsena42 cfba886
Merge branch 'master' into fix/clean-keychain-persistence-uninstall
jvsena42 335daf9
Merge branch 'master' into fix/clean-keychain-persistence-uninstall
jvsena42 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| import CryptoKit | ||
| import Foundation | ||
|
|
||
| class KeychainCrypto { | ||
| private static var cachedKey: SymmetricKey? | ||
| private static let keyFileName = ".keychain_encryption_key" | ||
|
|
||
| // Network-specific key path (matches existing patterns) | ||
| private static var keyFilePath: URL { | ||
| let networkName = switch Env.network { | ||
| case .bitcoin: | ||
| "bitcoin" | ||
| case .testnet: | ||
| "testnet" | ||
| case .signet: | ||
| "signet" | ||
| case .regtest: | ||
| "regtest" | ||
| } | ||
|
|
||
| return Env.appStorageUrl | ||
| .appendingPathComponent(networkName) | ||
| .appendingPathComponent(keyFileName) | ||
| } | ||
|
|
||
| // Get or create encryption key | ||
| static func getOrCreateKey() throws -> SymmetricKey { | ||
| // Return cached key if available | ||
| if let cached = cachedKey { | ||
| return cached | ||
| } | ||
|
|
||
| // Try to load existing key | ||
| if FileManager.default.fileExists(atPath: keyFilePath.path) { | ||
| let keyData = try Data(contentsOf: keyFilePath) | ||
| let key = SymmetricKey(data: keyData) | ||
| cachedKey = key | ||
| Logger.debug("Loaded encryption key from storage", context: "KeychainCrypto") | ||
| return key | ||
| } | ||
|
|
||
| // Create new key | ||
| let newKey = SymmetricKey(size: .bits256) | ||
| try saveKey(newKey) | ||
| cachedKey = newKey | ||
| Logger.info("Created new encryption key", context: "KeychainCrypto") | ||
| return newKey | ||
| } | ||
|
|
||
| private static func saveKey(_ key: SymmetricKey) throws { | ||
| // Ensure directory exists | ||
| let directory = keyFilePath.deletingLastPathComponent() | ||
| try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) | ||
|
|
||
| // Save key data with file protection | ||
| let keyData = key.withUnsafeBytes { Data($0) } | ||
| try keyData.write(to: keyFilePath, options: .completeFileProtection) | ||
|
|
||
| Logger.debug("Saved encryption key to \(keyFilePath.path)", context: "KeychainCrypto") | ||
| } | ||
|
|
||
| // Check if key exists | ||
| static func keyExists() -> Bool { | ||
| return FileManager.default.fileExists(atPath: keyFilePath.path) | ||
| } | ||
|
|
||
| // Delete key (used during wipe) | ||
| static func deleteKey() throws { | ||
| if FileManager.default.fileExists(atPath: keyFilePath.path) { | ||
| try FileManager.default.removeItem(at: keyFilePath) | ||
| cachedKey = nil | ||
| Logger.info("Deleted encryption key", context: "KeychainCrypto") | ||
| } | ||
| } | ||
|
|
||
| // Encrypt data before keychain storage | ||
| static func encrypt(_ data: Data) throws -> Data { | ||
| let key = try getOrCreateKey() | ||
| let sealedBox = try AES.GCM.seal(data, using: key) | ||
|
|
||
| // Combine nonce + ciphertext + tag into single Data blob | ||
| var combined = Data() | ||
| combined.append(sealedBox.nonce.withUnsafeBytes { Data($0) }) | ||
| combined.append(sealedBox.ciphertext) | ||
| combined.append(sealedBox.tag) | ||
|
|
||
| Logger.debug("Encrypted data (\(data.count) bytes → \(combined.count) bytes)", context: "KeychainCrypto") | ||
| return combined | ||
| } | ||
|
|
||
| // Decrypt data after keychain retrieval | ||
| static func decrypt(_ encryptedData: Data) throws -> Data { | ||
| let key = try getOrCreateKey() | ||
|
|
||
| // Extract components (nonce=12 bytes, tag=16 bytes, rest=ciphertext) | ||
| guard encryptedData.count >= 28 else { // 12 + 16 minimum | ||
| Logger.error("Invalid encrypted data: too short (\(encryptedData.count) bytes)", context: "KeychainCrypto") | ||
| throw KeychainCryptoError.invalidEncryptedData | ||
| } | ||
|
|
||
| let nonceData = encryptedData.prefix(12) | ||
| let tagData = encryptedData.suffix(16) | ||
| let ciphertextData = encryptedData.dropFirst(12).dropLast(16) | ||
|
|
||
| do { | ||
| let nonce = try AES.GCM.Nonce(data: nonceData) | ||
| let sealedBox = try AES.GCM.SealedBox(nonce: nonce, ciphertext: ciphertextData, tag: tagData) | ||
| let decryptedData = try AES.GCM.open(sealedBox, using: key) | ||
|
|
||
| Logger.debug("Decrypted data (\(encryptedData.count) bytes → \(decryptedData.count) bytes)", context: "KeychainCrypto") | ||
| return decryptedData | ||
| } catch { | ||
| Logger.error("Decryption failed: \(error.localizedDescription)", context: "KeychainCrypto") | ||
| throw KeychainCryptoError.decryptionFailed | ||
| } | ||
| } | ||
|
|
||
| enum KeychainCryptoError: Error { | ||
| case invalidEncryptedData | ||
| case keyNotFound | ||
| case decryptionFailed | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.