From f0a6f2e43b88d13795001a11e83afe5474079bc3 Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Sun, 24 Aug 2025 20:19:29 -0700 Subject: [PATCH] Save text (#632) --- Sources/Packages/Localizable.xcstrings | 22 +++++++++---------- .../CryptoKitMigrator.swift | 12 +++++----- .../SecureEnclaveSecret.swift | 4 ++-- .../SecureEnclaveStore.swift | 16 +++++++------- .../Secretive/Views/DeleteSecretView.swift | 14 ++++++++++-- Sources/Secretive/Views/EditSecretView.swift | 17 +++++++++++--- 6 files changed, 53 insertions(+), 32 deletions(-) diff --git a/Sources/Packages/Localizable.xcstrings b/Sources/Packages/Localizable.xcstrings index 2e29ce5..47553bb 100644 --- a/Sources/Packages/Localizable.xcstrings +++ b/Sources/Packages/Localizable.xcstrings @@ -2557,67 +2557,67 @@ "localizations" : { "ca" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Canvia el nom" } }, "de" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Umbenennen" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Rename" + "value" : "Save" } }, "fr" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Renommer" } }, "it" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Rinomina" } }, "ja" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "名前の変更" } }, "ko" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "이름 변경" } }, "pl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Zmień nazwę" } }, "pt-BR" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Renomear" } }, "ru" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Переименовать" } }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "重命名" } } diff --git a/Sources/Packages/Sources/SecureEnclaveSecretKit/CryptoKitMigrator.swift b/Sources/Packages/Sources/SecureEnclaveSecretKit/CryptoKitMigrator.swift index c03944a..ddcc042 100644 --- a/Sources/Packages/Sources/SecureEnclaveSecretKit/CryptoKitMigrator.swift +++ b/Sources/Packages/Sources/SecureEnclaveSecretKit/CryptoKitMigrator.swift @@ -46,16 +46,16 @@ extension SecureEnclave { let auth: AuthenticationRequirement = String(describing: accessControl) .contains("DeviceOwnerAuthentication") ? .presenceRequired : .unknown let parsed = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: tokenObjectID) - let secret = Secret(id: id, name: name, publicKey: parsed.publicKey.x963Representation, attributes: Attributes(keyType: .init(algorithm: .ecdsa, size: 256), authentication: auth)) + let secret = Secret(id: UUID().uuidString, name: name, publicKey: parsed.publicKey.x963Representation, attributes: Attributes(keyType: .init(algorithm: .ecdsa, size: 256), authentication: auth)) guard !migratedPublicKeys.contains(parsed.publicKey.x963Representation) else { logger.log("Skipping \(name), public key already present. Marking as migrated.") - try markMigrated(secret: secret) + try markMigrated(secret: secret, oldID: id) continue } logger.log("Migrating \(name).") try store.saveKey(tokenObjectID, name: name, attributes: secret.attributes) logger.log("Migrated \(name).") - try markMigrated(secret: secret) + try markMigrated(secret: secret, oldID: id) migrated = true } if migrated { @@ -65,13 +65,13 @@ extension SecureEnclave { - public func markMigrated(secret: Secret) throws { + public func markMigrated(secret: Secret, oldID: Data) throws { let updateQuery = KeychainDictionary([ kSecClass: kSecClassKey, - kSecAttrApplicationLabel: secret.id as CFData + kSecAttrApplicationLabel: secret.id ]) - let newID = secret.id + Constants.migrationMagicNumber + let newID = oldID + Constants.migrationMagicNumber let updatedAttributes = KeychainDictionary([ kSecAttrApplicationLabel: newID as CFData ]) diff --git a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveSecret.swift b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveSecret.swift index 27e782e..f193478 100644 --- a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveSecret.swift +++ b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveSecret.swift @@ -6,13 +6,13 @@ extension SecureEnclave { /// An implementation of Secret backed by the Secure Enclave. public struct Secret: SecretKit.Secret { - public let id: Data + public let id: String public let name: String public let publicKey: Data public let attributes: Attributes init( - id: Data, + id: String, name: String, publicKey: Data, attributes: Attributes diff --git a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift index a001c4f..4574e6f 100644 --- a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift +++ b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift @@ -48,9 +48,9 @@ extension SecureEnclave { kSecClass: Constants.keyClass, kSecAttrService: Constants.keyTag, kSecUseDataProtectionKeychain: true, - kSecAttrAccount: String(decoding: secret.id, as: UTF8.self), + kSecAttrAccount: secret.id, kSecReturnAttributes: true, - kSecReturnData: true + kSecReturnData: true, ]) var untyped: CFTypeRef? let status = SecItemCopyMatching(queryAttributes, &untyped) @@ -143,8 +143,7 @@ extension SecureEnclave { kSecClass: Constants.keyClass, kSecAttrService: Constants.keyTag, kSecUseDataProtectionKeychain: true, - kSecAttrAccount: String(decoding: secret.id, as: UTF8.self), - kSecAttrCanSign: true, + kSecAttrAccount: secret.id, ]) let status = SecItemDelete(deleteAttributes) if status != errSecSuccess { @@ -155,12 +154,14 @@ extension SecureEnclave { public func update(secret: Secret, name: String, attributes: Attributes) async throws { let updateQuery = KeychainDictionary([ - kSecClass: kSecClassKey, - kSecAttrApplicationLabel: secret.id as CFData + kSecClass: Constants.keyClass, + kSecAttrAccount: secret.id, ]) + let attributes = try JSONEncoder().encode(attributes) let updatedAttributes = KeychainDictionary([ kSecAttrLabel: name, + kSecAttrGeneric: attributes, ]) let status = SecItemUpdate(updateQuery, updatedAttributes) @@ -213,10 +214,9 @@ extension SecureEnclave.Store { do { let name = $0[kSecAttrLabel] as? String ?? String(localized: "unnamed_secret") guard let attributesData = $0[kSecAttrGeneric] as? Data, - let idString = $0[kSecAttrAccount] as? String else { + let id = $0[kSecAttrAccount] as? String else { throw MissingAttributesError() } - let id = Data(idString.utf8) let attributes = try JSONDecoder().decode(Attributes.self, from: attributesData) let keyData = $0[kSecValueData] as! Data let publicKey: Data diff --git a/Sources/Secretive/Views/DeleteSecretView.swift b/Sources/Secretive/Views/DeleteSecretView.swift index 00063d2..e53fda9 100644 --- a/Sources/Secretive/Views/DeleteSecretView.swift +++ b/Sources/Secretive/Views/DeleteSecretView.swift @@ -8,6 +8,7 @@ struct DeleteSecretView: View { var dismissalBlock: (Bool) -> () @State private var confirm = "" + @State var errorText: String? var body: some View { VStack { @@ -31,6 +32,11 @@ struct DeleteSecretView: View { } } } + if let errorText { + Text(verbatim: errorText) + .foregroundStyle(.red) + .font(.callout) + } HStack { Spacer() Button(.deleteConfirmationDeleteButton, action: delete) @@ -50,8 +56,12 @@ struct DeleteSecretView: View { func delete() { Task { - try! await store.delete(secret: secret) - dismissalBlock(true) + do { + try await store.delete(secret: secret) + dismissalBlock(true) + } catch { + errorText = error.localizedDescription + } } } diff --git a/Sources/Secretive/Views/EditSecretView.swift b/Sources/Secretive/Views/EditSecretView.swift index a4a7cba..606b130 100644 --- a/Sources/Secretive/Views/EditSecretView.swift +++ b/Sources/Secretive/Views/EditSecretView.swift @@ -9,6 +9,7 @@ struct EditSecretView: View { @State private var name: String @State private var publicKeyAttribution: String + @State var errorText: String? init(store: StoreType, secret: StoreType.SecretType, dismissalBlock: @escaping (Bool) -> ()) { self.store = store @@ -30,6 +31,11 @@ struct EditSecretView: View { .foregroundStyle(.secondary) } } + if let errorText { + Text(verbatim: errorText) + .foregroundStyle(.red) + .font(.callout) + } } HStack { Button(.editSaveButton, action: rename) @@ -37,7 +43,8 @@ struct EditSecretView: View { .keyboardShortcut(.return) Button(.editCancelButton) { dismissalBlock(false) - }.keyboardShortcut(.cancelAction) + } + .keyboardShortcut(.cancelAction) } .padding() } @@ -50,8 +57,12 @@ struct EditSecretView: View { attributes.publicKeyAttribution = publicKeyAttribution } Task { - try? await store.update(secret: secret, name: name, attributes: attributes) - dismissalBlock(true) + do { + try await store.update(secret: secret, name: name, attributes: attributes) + dismissalBlock(true) + } catch { + errorText = error.localizedDescription + } } } }