damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

commit e4860f3ba88ff8f95a87a0e73d120e3b349ffadf
parent 27fb4e797de6170ada27121c754af95d643e4186
Author: Bryan Montz <bryanmontz@me.com>
Date:   Thu,  4 May 2023 06:40:04 -0500

Replace Vault dependency with @KeychainStorage property wrapper

Changelog-Changed: replace Vault dependency with @KeychainStorage property wrapper
Closes: #1076

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 25++++++++-----------------
Mdamus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 9---------
Mdamus/Models/UserSettingsStore.swift | 115++++++++++++++++++-------------------------------------------------------------
Adamus/Util/KeychainStorage.swift | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus/Util/Keys.swift | 18++++++++----------
Mdamus/Views/Settings/TranslationSettingsView.swift | 79+------------------------------------------------------------------------------
AdamusTests/KeychainStorageTests.swift | 46++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 162 insertions(+), 203 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -258,6 +258,8 @@ 4CFF8F6D29CD022E008DB934 /* WideEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6C29CD022E008DB934 /* WideEventView.swift */; }; 4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE60CDC295E1C5E00105A1F /* Wallet.swift */; }; 50088DA129E8271A008A1FDF /* WebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50088DA029E8271A008A1FDF /* WebSocket.swift */; }; + 501F8C802A0220E1001AFC1D /* KeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C7F2A0220E1001AFC1D /* KeychainStorage.swift */; }; + 501F8C822A0224EB001AFC1D /* KeychainStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C812A0224EB001AFC1D /* KeychainStorageTests.swift */; }; 50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */; }; 50B5685329F97CB400A23243 /* CredentialHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B5685229F97CB400A23243 /* CredentialHandler.swift */; }; 5C42E78C29DB76D90086AAC1 /* EmptyUserSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */; }; @@ -268,7 +270,6 @@ 643EA5C8296B764E005081BB /* RelayFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 643EA5C7296B764E005081BB /* RelayFilterView.swift */; }; 647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647D9A8C2968520300A295DE /* SideMenuView.swift */; }; 64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FBD06E296255C400D9D3B2 /* Theme.swift */; }; - 6C7DE41F2955169800E66263 /* Vault in Frameworks */ = {isa = PBXBuildFile; productRef = 6C7DE41E2955169800E66263 /* Vault */; }; 7C0F392F29B57CAF0039859C /* Binding+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C0F392E29B57CAF0039859C /* Binding+.swift */; }; 7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C60CAEE298471A1009C80D6 /* CoreSVG.swift */; }; 7C902AE32981D55B002AB16E /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */; }; @@ -681,6 +682,8 @@ 4CFF8F6C29CD022E008DB934 /* WideEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WideEventView.swift; sourceTree = "<group>"; }; 4FE60CDC295E1C5E00105A1F /* Wallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wallet.swift; sourceTree = "<group>"; }; 50088DA029E8271A008A1FDF /* WebSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSocket.swift; sourceTree = "<group>"; }; + 501F8C7F2A0220E1001AFC1D /* KeychainStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorage.swift; sourceTree = "<group>"; }; + 501F8C812A0224EB001AFC1D /* KeychainStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorageTests.swift; sourceTree = "<group>"; }; 50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestTests.swift; sourceTree = "<group>"; }; 50B5685229F97CB400A23243 /* CredentialHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialHandler.swift; sourceTree = "<group>"; }; 5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyUserSearchView.swift; sourceTree = "<group>"; }; @@ -720,7 +723,6 @@ buildActionMask = 2147483647; files = ( 4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */, - 6C7DE41F2955169800E66263 /* Vault in Frameworks */, 4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1035,6 +1037,7 @@ 4C363AA728297703006E126D /* InsertSort.swift */, 4C477C9D282C3A4800033AA3 /* TipCounter.swift */, 4C285C8B28398BC6008A31F1 /* Keys.swift */, + 501F8C7F2A0220E1001AFC1D /* KeychainStorage.swift */, 4C90BD19283AA67F008EE7EF /* Bech32.swift */, 4C216F352870A9A700040376 /* InputDismissKeyboard.swift */, 3169CAEC294FCCFC00EE4006 /* Constants.swift */, @@ -1271,6 +1274,7 @@ 3A3040F229A91366008A0F29 /* ProfileViewTests.swift */, 3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */, 4C8D00D329E3C5D40036AF10 /* NIP19Tests.swift */, + 501F8C812A0224EB001AFC1D /* KeychainStorageTests.swift */, ); path = damusTests; sourceTree = "<group>"; @@ -1393,7 +1397,6 @@ packageProductDependencies = ( 4C649880286E0EE300EAE2B3 /* secp256k1 */, 4C06670328FC7EC500038D2A /* Kingfisher */, - 6C7DE41E2955169800E66263 /* Vault */, ); productName = damus; productReference = 4CE6DEE327F7A08100C66700 /* damus.app */; @@ -1498,7 +1501,6 @@ packageReferences = ( 4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */, 4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */, - 6C7DE41D2955169800E66263 /* XCRemoteSwiftPackageReference "Vault" */, ); productRefGroup = 4CE6DEE427F7A08100C66700 /* Products */; projectDirPath = ""; @@ -1789,6 +1791,7 @@ 4C75EFA427FA577B0006080F /* PostView.swift in Sources */, 4C30AC7229A5677A00E2BD5A /* NotificationsView.swift in Sources */, 4C1A9A2129DDD3E100516EAC /* KeySettingsView.swift in Sources */, + 501F8C802A0220E1001AFC1D /* KeychainStorage.swift in Sources */, 4C1A9A1D29DDCF9B00516EAC /* NotificationSettingsView.swift in Sources */, 4C75EFB528049D790006080F /* Relay.swift in Sources */, 4CEE2AF1280B216B00AB5EEF /* EventDetailView.swift in Sources */, @@ -1811,6 +1814,7 @@ 3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */, 4C8D00D429E3C5D40036AF10 /* NIP19Tests.swift in Sources */, 3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */, + 501F8C822A0224EB001AFC1D /* KeychainStorageTests.swift in Sources */, 3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */, DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */, 3A3040EF29A8FEE9008A0F29 /* EventDetailBarTests.swift in Sources */, @@ -2307,14 +2311,6 @@ revision = 40b4b38b3b1c83f7088c76189a742870e0ca06a9; }; }; - 6C7DE41D2955169800E66263 /* XCRemoteSwiftPackageReference "Vault" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/SparrowTek/Vault"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.0.0; - }; - }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -2328,11 +2324,6 @@ package = 4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */; productName = secp256k1; }; - 6C7DE41E2955169800E66263 /* Vault */ = { - isa = XCSwiftPackageProductDependency; - package = 6C7DE41D2955169800E66263 /* XCRemoteSwiftPackageReference "Vault" */; - productName = Vault; - }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 4CE6DEDB27F7A08100C66700 /* Project object */; diff --git a/damus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/damus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -16,15 +16,6 @@ "state" : { "revision" : "40b4b38b3b1c83f7088c76189a742870e0ca06a9" } - }, - { - "identity" : "vault", - "kind" : "remoteSourceControl", - "location" : "https://github.com/SparrowTek/Vault", - "state" : { - "revision" : "87db56c3c8b6421c65b0745f73e08b0dc56f79d4", - "version" : "1.0.3" - } } ], "version" : 2 diff --git a/damus/Models/UserSettingsStore.swift b/damus/Models/UserSettingsStore.swift @@ -6,7 +6,6 @@ // import Foundation -import Vault import UIKit let fallback_zap_amount = 1000 @@ -160,16 +159,11 @@ class UserSettingsStore: ObservableObject { var deepl_plan: DeepLPlan var deepl_api_key: String { - didSet { - do { - if deepl_api_key == "" { - try clearDeepLApiKey() - } else { - try saveDeepLApiKey(deepl_api_key) - } - } catch { - // No-op. - } + get { + return internal_deepl_api_key ?? "" + } + set { + internal_deepl_api_key = newValue == "" ? nil : newValue } } @@ -179,73 +173,34 @@ class UserSettingsStore: ObservableObject { @Setting(key: "libretranslate_url", default_value: "") var libretranslate_url: String - @Setting(key: "libretranslate_api_key", default_value: "") var libretranslate_api_key: String { - didSet { - do { - if libretranslate_api_key == "" { - try clearLibreTranslateApiKey() - } else { - try saveLibreTranslateApiKey(libretranslate_api_key) - } - } catch { - // No-op. - } + get { + return internal_libretranslate_api_key ?? "" } - } - - @Published var nokyctranslate_api_key: String { - didSet { - do { - if nokyctranslate_api_key == "" { - try clearNoKYCTranslateApiKey() - } else { - try saveNoKYCTranslateApiKey(nokyctranslate_api_key) - } - } catch { - // No-op. - } + set { + internal_libretranslate_api_key = newValue == "" ? nil : newValue } } - - init() { - do { - deepl_api_key = try Vault.getPrivateKey(keychainConfiguration: DamusDeepLKeychainConfiguration()) - } catch { - deepl_api_key = "" + + var nokyctranslate_api_key: String { + get { + return internal_nokyctranslate_api_key ?? "" } - - do { - nokyctranslate_api_key = try Vault.getPrivateKey(keychainConfiguration: DamusNoKYCTranslateKeychainConfiguration()) - } catch { - nokyctranslate_api_key = "" + set { + internal_nokyctranslate_api_key = newValue == "" ? nil : newValue } - - } - - private func saveLibreTranslateApiKey(_ apiKey: String) throws { - try Vault.savePrivateKey(apiKey, keychainConfiguration: DamusLibreTranslateKeychainConfiguration()) - } - - private func clearLibreTranslateApiKey() throws { - try Vault.deletePrivateKey(keychainConfiguration: DamusLibreTranslateKeychainConfiguration()) - } - - private func saveNoKYCTranslateApiKey(_ apiKey: String) throws { - try Vault.savePrivateKey(apiKey, keychainConfiguration: DamusNoKYCTranslateKeychainConfiguration()) } - private func clearNoKYCTranslateApiKey() throws { - try Vault.deletePrivateKey(keychainConfiguration: DamusNoKYCTranslateKeychainConfiguration()) - } + // These internal keys are necessary because entries in the keychain need to be Optional, + // but the translation view needs non-Optional String in order to use them as Bindings. + @KeychainStorage(account: "deepl_apikey") + var internal_deepl_api_key: String? - private func saveDeepLApiKey(_ apiKey: String) throws { - try Vault.savePrivateKey(apiKey, keychainConfiguration: DamusDeepLKeychainConfiguration()) - } - - private func clearDeepLApiKey() throws { - try Vault.deletePrivateKey(keychainConfiguration: DamusDeepLKeychainConfiguration()) - } + @KeychainStorage(account: "nokyctranslate_apikey") + var internal_nokyctranslate_api_key: String? + + @KeychainStorage(account: "libretranslate_apikey") + var internal_libretranslate_api_key: String? var can_translate: Bool { switch translation_service { @@ -254,31 +209,13 @@ class UserSettingsStore: ObservableObject { case .libretranslate: return URLComponents(string: libretranslate_url) != nil case .deepl: - return deepl_api_key != "" + return internal_deepl_api_key != nil case .nokyctranslate: - return nokyctranslate_api_key != "" + return internal_nokyctranslate_api_key != nil } } } -struct DamusLibreTranslateKeychainConfiguration: KeychainConfiguration { - var serviceName = "damus" - var accessGroup: String? = nil - var accountName = "libretranslate_apikey" -} - -struct DamusDeepLKeychainConfiguration: KeychainConfiguration { - var serviceName = "damus" - var accessGroup: String? = nil - var accountName = "deepl_apikey" -} - -struct DamusNoKYCTranslateKeychainConfiguration: KeychainConfiguration { - var serviceName = "damus" - var accessGroup: String? = nil - var accountName = "nokyctranslate_apikey" -} - func pk_setting_key(_ pubkey: String, key: String) -> String { return "\(pubkey)_\(key)" } diff --git a/damus/Util/KeychainStorage.swift b/damus/Util/KeychainStorage.swift @@ -0,0 +1,73 @@ +// +// KeychainStorage.swift +// damus +// +// Created by Bryan Montz on 5/2/23. +// + +import Foundation +import Security + +@propertyWrapper struct KeychainStorage { + let account: String + private let service = "damus" + + var wrappedValue: String? { + get { + let query = [ + kSecAttrService: service, + kSecAttrAccount: account, + kSecClass: kSecClassGenericPassword, + kSecReturnData: true, + kSecMatchLimit: kSecMatchLimitOne + ] as [CFString: Any] as CFDictionary + + var result: AnyObject? + let status = SecItemCopyMatching(query, &result) + + if status == errSecSuccess, let data = result as? Data { + return String(data: data, encoding: .utf8) + } else { + return nil + } + } + set { + if let newValue { + let query = [ + kSecAttrService: service, + kSecAttrAccount: account, + kSecClass: kSecClassGenericPassword, + kSecValueData: newValue.data(using: .utf8) as Any + ] as [CFString: Any] as CFDictionary + + var status = SecItemAdd(query, nil) + + if status == errSecDuplicateItem { + let query = [ + kSecAttrService: service, + kSecAttrAccount: account, + kSecClass: kSecClassGenericPassword + ] as [CFString: Any] as CFDictionary + + let updates = [ + kSecValueData: newValue.data(using: .utf8) as Any + ] as CFDictionary + + status = SecItemUpdate(query, updates) + } + } else { + let query = [ + kSecAttrService: service, + kSecAttrAccount: account, + kSecClass: kSecClassGenericPassword + ] as [CFString: Any] as CFDictionary + + _ = SecItemDelete(query) + } + } + } + + init(account: String) { + self.account = account + } +} diff --git a/damus/Util/Keys.swift b/damus/Util/Keys.swift @@ -7,7 +7,6 @@ import Foundation import secp256k1 -import Vault let PUBKEY_HRP = "npub" let PRIVKEY_HRP = "nsec" @@ -44,12 +43,6 @@ enum Bech32Key { case sec(String) } -struct DamusKeychainConfiguration: KeychainConfiguration { - var serviceName = "damus" - var accessGroup: String? = nil - var accountName = "privkey" -} - func decode_bech32_key(_ key: String) -> Bech32Key? { guard let decoded = try? bech32_decode(key) else { return nil @@ -114,12 +107,17 @@ func save_pubkey(pubkey: String) { UserDefaults.standard.set(pubkey, forKey: "pubkey") } +enum Keys { + @KeychainStorage(account: "privkey") + static var privkey: String? +} + func save_privkey(privkey: String) throws { - try Vault.savePrivateKey(privkey, keychainConfiguration: DamusKeychainConfiguration()) + Keys.privkey = privkey } func clear_saved_privkey() throws { - try Vault.deletePrivateKey(keychainConfiguration: DamusKeychainConfiguration()) + Keys.privkey = nil } func clear_saved_pubkey() { @@ -154,7 +152,7 @@ func get_saved_pubkey() -> String? { } func get_saved_privkey() -> String? { - let mkey = try? Vault.getPrivateKey(keychainConfiguration: DamusKeychainConfiguration()); + let mkey = Keys.privkey return mkey.map { $0.trimmingCharacters(in: .whitespaces) } } diff --git a/damus/Views/Settings/TranslationSettingsView.swift b/damus/Views/Settings/TranslationSettingsView.swift @@ -79,9 +79,7 @@ struct TranslationSettingsView: View { if settings.translation_service != .none { Toggle(NSLocalizedString("Automatically translate notes", comment: "Toggle to automatically translate notes."), isOn: $settings.auto_translate) .toggleStyle(.switch) - } - - if settings.translation_service != .none { + Toggle(NSLocalizedString("Translate DMs", comment: "Toggle to translate direct messages."), isOn: $settings.translate_dms) .toggleStyle(.switch) } @@ -92,81 +90,6 @@ struct TranslationSettingsView: View { dismiss() } } - - var libretranslate_view: some View { - VStack { - Picker(NSLocalizedString("Server", comment: "Prompt selection of LibreTranslate server to perform machine translations on notes"), selection: $settings.libretranslate_server) { - ForEach(LibreTranslateServer.allCases, id: \.self) { server in - Text(server.model.displayName) - .tag(server.model.tag) - } - } - - TextField(NSLocalizedString("URL", comment: "Example URL to LibreTranslate server"), text: $settings.libretranslate_url) - .disableAutocorrection(true) - .disabled(settings.libretranslate_server != .custom) - .autocapitalization(UITextAutocapitalizationType.none) - HStack { - let libretranslate_api_key_placeholder = NSLocalizedString("API Key (optional)", comment: "Prompt for optional entry of API Key to use translation server.") - if show_api_key { - TextField(libretranslate_api_key_placeholder, text: $settings.libretranslate_api_key) - .disableAutocorrection(true) - .autocapitalization(UITextAutocapitalizationType.none) - if settings.libretranslate_api_key != "" { - Button(NSLocalizedString("Hide API Key", comment: "Button to hide the LibreTranslate server API key.")) { - show_api_key = false - } - } - } else { - SecureField(libretranslate_api_key_placeholder, text: $settings.libretranslate_api_key) - .disableAutocorrection(true) - .autocapitalization(UITextAutocapitalizationType.none) - if settings.libretranslate_api_key != "" { - Button(NSLocalizedString("Show API Key", comment: "Button to show the LibreTranslate server API key.")) { - show_api_key = true - } - } - } - } - } - } - - var deepl_view: some View { - VStack { - Picker(NSLocalizedString("Plan", comment: "Prompt selection of DeepL subscription plan to perform machine translations on notes"), selection: $settings.deepl_plan) { - ForEach(DeepLPlan.allCases, id: \.self) { server in - Text(server.model.displayName) - .tag(server.model.tag) - } - } - - HStack { - let deepl_api_key_placeholder = NSLocalizedString("API Key (required)", comment: "Prompt for required entry of API Key to use translation server.") - if show_api_key { - TextField(deepl_api_key_placeholder, text: $settings.deepl_api_key) - .disableAutocorrection(true) - .autocapitalization(UITextAutocapitalizationType.none) - if settings.deepl_api_key != "" { - Button(NSLocalizedString("Hide API Key", comment: "Button to hide the DeepL translation API key.")) { - show_api_key = false - } - } - } else { - SecureField(deepl_api_key_placeholder, text: $settings.deepl_api_key) - .disableAutocorrection(true) - .autocapitalization(UITextAutocapitalizationType.none) - if settings.deepl_api_key != "" { - Button(NSLocalizedString("Show API Key", comment: "Button to show the DeepL translation API key.")) { - show_api_key = true - } - } - } - if settings.deepl_api_key == "" { - Link(NSLocalizedString("Get API Key", comment: "Button to navigate to DeepL website to get a translation API key."), destination: URL(string: "https://www.deepl.com/pro-api")!) - } - } - } - } } struct TranslationSettingsView_Previews: PreviewProvider { diff --git a/damusTests/KeychainStorageTests.swift b/damusTests/KeychainStorageTests.swift @@ -0,0 +1,46 @@ +// +// KeychainStorageTests.swift +// damusTests +// +// Created by Bryan Montz on 5/3/23. +// + +import XCTest +@testable import damus +import Security + +final class KeychainStorageTests: XCTestCase { + @KeychainStorage(account: "test-keyname") + var secret: String? + + override func tearDownWithError() throws { + secret = nil + } + + func testWriteToKeychain() throws { + // write a secret to the keychain using the property wrapper's setter + secret = "super-secure-key" + + // verify it exists in the keychain using the property wrapper's getter + XCTAssertEqual(secret, "super-secure-key") + + // verify it exists in the keychain directly + let query = [ + kSecAttrService: "damus", + kSecAttrAccount: "test-keyname", + kSecClass: kSecClassGenericPassword, + kSecReturnData: true, + kSecMatchLimit: kSecMatchLimitOne + ] as [CFString: Any] as CFDictionary + + var result: AnyObject? + let status = SecItemCopyMatching(query, &result) + XCTAssertEqual(status, errSecSuccess) + + let data = try XCTUnwrap(result as? Data) + let the_secret = String(data: data, encoding: .utf8) + + XCTAssertEqual(the_secret, "super-secure-key") + } + +}