1
0
mirror of git://jb55.com/damus synced 2024-10-01 09:20:47 +00:00

Replace Vault dependency with @KeychainStorage property wrapper

Changelog-Changed: replace Vault dependency with @KeychainStorage property wrapper
Closes: #1076
This commit is contained in:
Bryan Montz 2023-05-04 06:40:04 -05:00 committed by William Casarin
parent 27fb4e797d
commit e4860f3ba8
7 changed files with 162 additions and 203 deletions

View File

@ -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 */;

View File

@ -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

View File

@ -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 ?? ""
}
set {
internal_libretranslate_api_key = newValue == "" ? nil : newValue
}
}
@Published var nokyctranslate_api_key: String {
didSet {
do {
if nokyctranslate_api_key == "" {
try clearNoKYCTranslateApiKey()
} else {
try saveNoKYCTranslateApiKey(nokyctranslate_api_key)
}
} catch {
// No-op.
}
var nokyctranslate_api_key: String {
get {
return internal_nokyctranslate_api_key ?? ""
}
}
init() {
do {
deepl_api_key = try Vault.getPrivateKey(keychainConfiguration: DamusDeepLKeychainConfiguration())
} catch {
deepl_api_key = ""
set {
internal_nokyctranslate_api_key = newValue == "" ? nil : newValue
}
do {
nokyctranslate_api_key = try Vault.getPrivateKey(keychainConfiguration: DamusNoKYCTranslateKeychainConfiguration())
} catch {
nokyctranslate_api_key = ""
}
}
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)"
}

View File

@ -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
}
}

View File

@ -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) }
}

View File

@ -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 {

View File

@ -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")
}
}