Save text (#632)

This commit is contained in:
Max Goedjen 2025-08-24 20:19:29 -07:00 committed by GitHub
parent 828c61cb2f
commit f0a6f2e43b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 53 additions and 32 deletions

View File

@ -2557,67 +2557,67 @@
"localizations" : { "localizations" : {
"ca" : { "ca" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "Canvia el nom" "value" : "Canvia el nom"
} }
}, },
"de" : { "de" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "Umbenennen" "value" : "Umbenennen"
} }
}, },
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "Rename" "value" : "Save"
} }
}, },
"fr" : { "fr" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "Renommer" "value" : "Renommer"
} }
}, },
"it" : { "it" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "Rinomina" "value" : "Rinomina"
} }
}, },
"ja" : { "ja" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "名前の変更" "value" : "名前の変更"
} }
}, },
"ko" : { "ko" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "이름 변경" "value" : "이름 변경"
} }
}, },
"pl" : { "pl" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "Zmień nazwę" "value" : "Zmień nazwę"
} }
}, },
"pt-BR" : { "pt-BR" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "Renomear" "value" : "Renomear"
} }
}, },
"ru" : { "ru" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "Переименовать" "value" : "Переименовать"
} }
}, },
"zh-Hans" : { "zh-Hans" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "needs_review",
"value" : "重命名" "value" : "重命名"
} }
} }

View File

@ -46,16 +46,16 @@ extension SecureEnclave {
let auth: AuthenticationRequirement = String(describing: accessControl) let auth: AuthenticationRequirement = String(describing: accessControl)
.contains("DeviceOwnerAuthentication") ? .presenceRequired : .unknown .contains("DeviceOwnerAuthentication") ? .presenceRequired : .unknown
let parsed = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: tokenObjectID) 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 { guard !migratedPublicKeys.contains(parsed.publicKey.x963Representation) else {
logger.log("Skipping \(name), public key already present. Marking as migrated.") logger.log("Skipping \(name), public key already present. Marking as migrated.")
try markMigrated(secret: secret) try markMigrated(secret: secret, oldID: id)
continue continue
} }
logger.log("Migrating \(name).") logger.log("Migrating \(name).")
try store.saveKey(tokenObjectID, name: name, attributes: secret.attributes) try store.saveKey(tokenObjectID, name: name, attributes: secret.attributes)
logger.log("Migrated \(name).") logger.log("Migrated \(name).")
try markMigrated(secret: secret) try markMigrated(secret: secret, oldID: id)
migrated = true migrated = true
} }
if migrated { 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([ let updateQuery = KeychainDictionary([
kSecClass: kSecClassKey, kSecClass: kSecClassKey,
kSecAttrApplicationLabel: secret.id as CFData kSecAttrApplicationLabel: secret.id
]) ])
let newID = secret.id + Constants.migrationMagicNumber let newID = oldID + Constants.migrationMagicNumber
let updatedAttributes = KeychainDictionary([ let updatedAttributes = KeychainDictionary([
kSecAttrApplicationLabel: newID as CFData kSecAttrApplicationLabel: newID as CFData
]) ])

View File

@ -6,13 +6,13 @@ extension SecureEnclave {
/// An implementation of Secret backed by the Secure Enclave. /// An implementation of Secret backed by the Secure Enclave.
public struct Secret: SecretKit.Secret { public struct Secret: SecretKit.Secret {
public let id: Data public let id: String
public let name: String public let name: String
public let publicKey: Data public let publicKey: Data
public let attributes: Attributes public let attributes: Attributes
init( init(
id: Data, id: String,
name: String, name: String,
publicKey: Data, publicKey: Data,
attributes: Attributes attributes: Attributes

View File

@ -48,9 +48,9 @@ extension SecureEnclave {
kSecClass: Constants.keyClass, kSecClass: Constants.keyClass,
kSecAttrService: Constants.keyTag, kSecAttrService: Constants.keyTag,
kSecUseDataProtectionKeychain: true, kSecUseDataProtectionKeychain: true,
kSecAttrAccount: String(decoding: secret.id, as: UTF8.self), kSecAttrAccount: secret.id,
kSecReturnAttributes: true, kSecReturnAttributes: true,
kSecReturnData: true kSecReturnData: true,
]) ])
var untyped: CFTypeRef? var untyped: CFTypeRef?
let status = SecItemCopyMatching(queryAttributes, &untyped) let status = SecItemCopyMatching(queryAttributes, &untyped)
@ -143,8 +143,7 @@ extension SecureEnclave {
kSecClass: Constants.keyClass, kSecClass: Constants.keyClass,
kSecAttrService: Constants.keyTag, kSecAttrService: Constants.keyTag,
kSecUseDataProtectionKeychain: true, kSecUseDataProtectionKeychain: true,
kSecAttrAccount: String(decoding: secret.id, as: UTF8.self), kSecAttrAccount: secret.id,
kSecAttrCanSign: true,
]) ])
let status = SecItemDelete(deleteAttributes) let status = SecItemDelete(deleteAttributes)
if status != errSecSuccess { if status != errSecSuccess {
@ -155,12 +154,14 @@ extension SecureEnclave {
public func update(secret: Secret, name: String, attributes: Attributes) async throws { public func update(secret: Secret, name: String, attributes: Attributes) async throws {
let updateQuery = KeychainDictionary([ let updateQuery = KeychainDictionary([
kSecClass: kSecClassKey, kSecClass: Constants.keyClass,
kSecAttrApplicationLabel: secret.id as CFData kSecAttrAccount: secret.id,
]) ])
let attributes = try JSONEncoder().encode(attributes)
let updatedAttributes = KeychainDictionary([ let updatedAttributes = KeychainDictionary([
kSecAttrLabel: name, kSecAttrLabel: name,
kSecAttrGeneric: attributes,
]) ])
let status = SecItemUpdate(updateQuery, updatedAttributes) let status = SecItemUpdate(updateQuery, updatedAttributes)
@ -213,10 +214,9 @@ extension SecureEnclave.Store {
do { do {
let name = $0[kSecAttrLabel] as? String ?? String(localized: "unnamed_secret") let name = $0[kSecAttrLabel] as? String ?? String(localized: "unnamed_secret")
guard let attributesData = $0[kSecAttrGeneric] as? Data, guard let attributesData = $0[kSecAttrGeneric] as? Data,
let idString = $0[kSecAttrAccount] as? String else { let id = $0[kSecAttrAccount] as? String else {
throw MissingAttributesError() throw MissingAttributesError()
} }
let id = Data(idString.utf8)
let attributes = try JSONDecoder().decode(Attributes.self, from: attributesData) let attributes = try JSONDecoder().decode(Attributes.self, from: attributesData)
let keyData = $0[kSecValueData] as! Data let keyData = $0[kSecValueData] as! Data
let publicKey: Data let publicKey: Data

View File

@ -8,6 +8,7 @@ struct DeleteSecretView<StoreType: SecretStoreModifiable>: View {
var dismissalBlock: (Bool) -> () var dismissalBlock: (Bool) -> ()
@State private var confirm = "" @State private var confirm = ""
@State var errorText: String?
var body: some View { var body: some View {
VStack { VStack {
@ -31,6 +32,11 @@ struct DeleteSecretView<StoreType: SecretStoreModifiable>: View {
} }
} }
} }
if let errorText {
Text(verbatim: errorText)
.foregroundStyle(.red)
.font(.callout)
}
HStack { HStack {
Spacer() Spacer()
Button(.deleteConfirmationDeleteButton, action: delete) Button(.deleteConfirmationDeleteButton, action: delete)
@ -50,8 +56,12 @@ struct DeleteSecretView<StoreType: SecretStoreModifiable>: View {
func delete() { func delete() {
Task { Task {
try! await store.delete(secret: secret) do {
dismissalBlock(true) try await store.delete(secret: secret)
dismissalBlock(true)
} catch {
errorText = error.localizedDescription
}
} }
} }

View File

@ -9,6 +9,7 @@ struct EditSecretView<StoreType: SecretStoreModifiable>: View {
@State private var name: String @State private var name: String
@State private var publicKeyAttribution: String @State private var publicKeyAttribution: String
@State var errorText: String?
init(store: StoreType, secret: StoreType.SecretType, dismissalBlock: @escaping (Bool) -> ()) { init(store: StoreType, secret: StoreType.SecretType, dismissalBlock: @escaping (Bool) -> ()) {
self.store = store self.store = store
@ -30,6 +31,11 @@ struct EditSecretView<StoreType: SecretStoreModifiable>: View {
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
} }
if let errorText {
Text(verbatim: errorText)
.foregroundStyle(.red)
.font(.callout)
}
} }
HStack { HStack {
Button(.editSaveButton, action: rename) Button(.editSaveButton, action: rename)
@ -37,7 +43,8 @@ struct EditSecretView<StoreType: SecretStoreModifiable>: View {
.keyboardShortcut(.return) .keyboardShortcut(.return)
Button(.editCancelButton) { Button(.editCancelButton) {
dismissalBlock(false) dismissalBlock(false)
}.keyboardShortcut(.cancelAction) }
.keyboardShortcut(.cancelAction)
} }
.padding() .padding()
} }
@ -50,8 +57,12 @@ struct EditSecretView<StoreType: SecretStoreModifiable>: View {
attributes.publicKeyAttribution = publicKeyAttribution attributes.publicKeyAttribution = publicKeyAttribution
} }
Task { Task {
try? await store.update(secret: secret, name: name, attributes: attributes) do {
dismissalBlock(true) try await store.update(secret: secret, name: name, attributes: attributes)
dismissalBlock(true)
} catch {
errorText = error.localizedDescription
}
} }
} }
} }