mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-24 17:20:59 +00:00
Modularize Keychain and Secrets
This commit is contained in:
parent
06b84a0aa7
commit
f6f065e143
18 changed files with 173 additions and 62 deletions
5
Keychain/.gitignore
vendored
Normal file
5
Keychain/.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
.DS_Store
|
||||||
|
/.build
|
||||||
|
/Packages
|
||||||
|
/*.xcodeproj
|
||||||
|
xcuserdata/
|
31
Keychain/Package.swift
Normal file
31
Keychain/Package.swift
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
// swift-tools-version:5.3
|
||||||
|
|
||||||
|
import PackageDescription
|
||||||
|
|
||||||
|
let package = Package(
|
||||||
|
name: "Keychain",
|
||||||
|
platforms: [
|
||||||
|
.iOS(.v14),
|
||||||
|
.macOS(.v11)
|
||||||
|
],
|
||||||
|
products: [
|
||||||
|
.library(
|
||||||
|
name: "Keychain",
|
||||||
|
targets: ["Keychain"]),
|
||||||
|
.library(
|
||||||
|
name: "MockKeychain",
|
||||||
|
targets: ["MockKeychain"])
|
||||||
|
],
|
||||||
|
dependencies: [],
|
||||||
|
targets: [
|
||||||
|
.target(
|
||||||
|
name: "Keychain",
|
||||||
|
dependencies: []),
|
||||||
|
.target(
|
||||||
|
name: "MockKeychain",
|
||||||
|
dependencies: ["Keychain"]),
|
||||||
|
.testTarget(
|
||||||
|
name: "KeychainTests",
|
||||||
|
dependencies: ["MockKeychain"])
|
||||||
|
]
|
||||||
|
)
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public protocol KeychainService {
|
public protocol Keychain {
|
||||||
static func setGenericPassword(data: Data, forAccount key: String, service: String) throws
|
static func setGenericPassword(data: Data, forAccount key: String, service: String) throws
|
||||||
static func deleteGenericPassword(account: String, service: String) throws
|
static func deleteGenericPassword(account: String, service: String) throws
|
||||||
static func getGenericPassword(account: String, service: String) throws -> Data?
|
static func getGenericPassword(account: String, service: String) throws -> Data?
|
||||||
|
@ -11,9 +11,9 @@ public protocol KeychainService {
|
||||||
static func deleteKey(applicationTag: String) throws
|
static func deleteKey(applicationTag: String) throws
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct LiveKeychainService {}
|
public struct LiveKeychain {}
|
||||||
|
|
||||||
extension LiveKeychainService: KeychainService {
|
extension LiveKeychain: Keychain {
|
||||||
public static func setGenericPassword(data: Data, forAccount account: String, service: String) throws {
|
public static func setGenericPassword(data: Data, forAccount account: String, service: String) throws {
|
||||||
var query = genericPasswordQueryDictionary(account: account, service: service)
|
var query = genericPasswordQueryDictionary(account: account, service: service)
|
||||||
|
|
||||||
|
@ -115,7 +115,7 @@ extension LiveKeychainService: KeychainService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension LiveKeychainService {
|
private extension LiveKeychain {
|
||||||
static func genericPasswordQueryDictionary(account: String, service: String) -> [String: Any] {
|
static func genericPasswordQueryDictionary(account: String, service: String) -> [String: Any] {
|
||||||
[kSecAttrService as String: service,
|
[kSecAttrService as String: service,
|
||||||
kSecAttrAccount as String: account,
|
kSecAttrAccount as String: account,
|
|
@ -1,17 +1,17 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import ServiceLayer
|
import Keychain
|
||||||
|
|
||||||
public struct MockKeychainService {}
|
public struct MockKeychain {}
|
||||||
|
|
||||||
public extension MockKeychainService {
|
public extension MockKeychain {
|
||||||
static func reset() {
|
static func reset() {
|
||||||
items = [String: Data]()
|
items = [String: Data]()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MockKeychainService: KeychainService {
|
extension MockKeychain: Keychain {
|
||||||
public static func setGenericPassword(data: Data, forAccount key: String, service: String) throws {
|
public static func setGenericPassword(data: Data, forAccount key: String, service: String) throws {
|
||||||
items[key] = data
|
items[key] = data
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,6 @@ extension MockKeychainService: KeychainService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension MockKeychainService {
|
private extension MockKeychain {
|
||||||
static var items = [String: Data]()
|
static var items = [String: Data]()
|
||||||
}
|
}
|
10
Keychain/Tests/KeychainTests/KeychainTests.swift
Normal file
10
Keychain/Tests/KeychainTests/KeychainTests.swift
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import XCTest
|
||||||
|
@testable import Keychain
|
||||||
|
|
||||||
|
final class KeychainTests: XCTestCase {
|
||||||
|
func testExample() {
|
||||||
|
// This is an example of a functional test case.
|
||||||
|
// Use XCTAssert and related functions to verify your tests produce the correct
|
||||||
|
// results.
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,18 +7,19 @@
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
D0175CAC24FE2D6300B085F6 /* PreviewViewModels in Frameworks */ = {isa = PBXBuildFile; productRef = D0175CAB24FE2D6300B085F6 /* PreviewViewModels */; };
|
|
||||||
D01F41D724F880C400D55A2D /* StatusTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D01F41D424F880C400D55A2D /* StatusTableViewCell.xib */; };
|
D01F41D724F880C400D55A2D /* StatusTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D01F41D424F880C400D55A2D /* StatusTableViewCell.xib */; };
|
||||||
D01F41D824F880C400D55A2D /* StatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41D524F880C400D55A2D /* StatusTableViewCell.swift */; };
|
D01F41D824F880C400D55A2D /* StatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41D524F880C400D55A2D /* StatusTableViewCell.swift */; };
|
||||||
D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41D624F880C400D55A2D /* TouchFallthroughTextView.swift */; };
|
D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41D624F880C400D55A2D /* TouchFallthroughTextView.swift */; };
|
||||||
D01F41E424F8889700D55A2D /* AttachmentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41E224F8889700D55A2D /* AttachmentsView.swift */; };
|
D01F41E424F8889700D55A2D /* AttachmentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41E224F8889700D55A2D /* AttachmentsView.swift */; };
|
||||||
D06B492324D4611300642749 /* KingfisherSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = D06B492224D4611300642749 /* KingfisherSwiftUI */; };
|
D06B492324D4611300642749 /* KingfisherSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = D06B492224D4611300642749 /* KingfisherSwiftUI */; };
|
||||||
D0BDF66B24FD7CEC00C7FA1C /* ServiceLayer in Frameworks */ = {isa = PBXBuildFile; productRef = D0BDF66A24FD7CEC00C7FA1C /* ServiceLayer */; };
|
|
||||||
D0BEB1F324F8EE8C001B0F04 /* AttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1F224F8EE8C001B0F04 /* AttachmentView.swift */; };
|
D0BEB1F324F8EE8C001B0F04 /* AttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1F224F8EE8C001B0F04 /* AttachmentView.swift */; };
|
||||||
D0BEB1F724F9A84B001B0F04 /* LoadingTableFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1F624F9A84B001B0F04 /* LoadingTableFooterView.swift */; };
|
D0BEB1F724F9A84B001B0F04 /* LoadingTableFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1F624F9A84B001B0F04 /* LoadingTableFooterView.swift */; };
|
||||||
D0BEB1FF24F9E5BB001B0F04 /* ListsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */; };
|
D0BEB1FF24F9E5BB001B0F04 /* ListsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */; };
|
||||||
D0BEB20524FA1107001B0F04 /* FiltersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB20424FA1107001B0F04 /* FiltersView.swift */; };
|
D0BEB20524FA1107001B0F04 /* FiltersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB20424FA1107001B0F04 /* FiltersView.swift */; };
|
||||||
D0BEB21124FA2A91001B0F04 /* EditFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB21024FA2A90001B0F04 /* EditFilterView.swift */; };
|
D0BEB21124FA2A91001B0F04 /* EditFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB21024FA2A90001B0F04 /* EditFilterView.swift */; };
|
||||||
|
D0BECB982501C0FC002C1B13 /* Secrets in Frameworks */ = {isa = PBXBuildFile; productRef = D0BECB972501C0FC002C1B13 /* Secrets */; };
|
||||||
|
D0BECB9A2501C15F002C1B13 /* Mastodon in Frameworks */ = {isa = PBXBuildFile; productRef = D0BECB992501C15F002C1B13 /* Mastodon */; };
|
||||||
|
D0BECB9C2501C731002C1B13 /* PreviewViewModels in Frameworks */ = {isa = PBXBuildFile; productRef = D0BECB9B2501C731002C1B13 /* PreviewViewModels */; };
|
||||||
D0C7D49724F7616A001EBDBB /* IdentitiesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42224F76169001EBDBB /* IdentitiesView.swift */; };
|
D0C7D49724F7616A001EBDBB /* IdentitiesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42224F76169001EBDBB /* IdentitiesView.swift */; };
|
||||||
D0C7D49824F7616A001EBDBB /* CustomEmojiText.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42324F76169001EBDBB /* CustomEmojiText.swift */; };
|
D0C7D49824F7616A001EBDBB /* CustomEmojiText.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42324F76169001EBDBB /* CustomEmojiText.swift */; };
|
||||||
D0C7D49924F7616A001EBDBB /* AddIdentityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42424F76169001EBDBB /* AddIdentityView.swift */; };
|
D0C7D49924F7616A001EBDBB /* AddIdentityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42424F76169001EBDBB /* AddIdentityView.swift */; };
|
||||||
|
@ -92,6 +93,8 @@
|
||||||
D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListsView.swift; sourceTree = "<group>"; };
|
D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListsView.swift; sourceTree = "<group>"; };
|
||||||
D0BEB20424FA1107001B0F04 /* FiltersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersView.swift; sourceTree = "<group>"; };
|
D0BEB20424FA1107001B0F04 /* FiltersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersView.swift; sourceTree = "<group>"; };
|
||||||
D0BEB21024FA2A90001B0F04 /* EditFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditFilterView.swift; sourceTree = "<group>"; };
|
D0BEB21024FA2A90001B0F04 /* EditFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditFilterView.swift; sourceTree = "<group>"; };
|
||||||
|
D0BECB952501B3DD002C1B13 /* Keychain */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Keychain; sourceTree = "<group>"; };
|
||||||
|
D0BECB962501BCE0002C1B13 /* Secrets */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Secrets; sourceTree = "<group>"; };
|
||||||
D0BFDAF524FC7C5300C86618 /* HTTP */ = {isa = PBXFileReference; lastKnownFileType = folder; path = HTTP; sourceTree = "<group>"; };
|
D0BFDAF524FC7C5300C86618 /* HTTP */ = {isa = PBXFileReference; lastKnownFileType = folder; path = HTTP; sourceTree = "<group>"; };
|
||||||
D0C7D41E24F76169001EBDBB /* Metatext.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Metatext.entitlements; sourceTree = "<group>"; };
|
D0C7D41E24F76169001EBDBB /* Metatext.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Metatext.entitlements; sourceTree = "<group>"; };
|
||||||
D0C7D41F24F76169001EBDBB /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
D0C7D41F24F76169001EBDBB /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
@ -132,7 +135,7 @@
|
||||||
files = (
|
files = (
|
||||||
D06B492324D4611300642749 /* KingfisherSwiftUI in Frameworks */,
|
D06B492324D4611300642749 /* KingfisherSwiftUI in Frameworks */,
|
||||||
D0E2C1D124FD97F000854680 /* ViewModels in Frameworks */,
|
D0E2C1D124FD97F000854680 /* ViewModels in Frameworks */,
|
||||||
D0175CAC24FE2D6300B085F6 /* PreviewViewModels in Frameworks */,
|
D0BECB9C2501C731002C1B13 /* PreviewViewModels in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -147,7 +150,8 @@
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
D0BDF66B24FD7CEC00C7FA1C /* ServiceLayer in Frameworks */,
|
D0BECB982501C0FC002C1B13 /* Secrets in Frameworks */,
|
||||||
|
D0BECB9A2501C15F002C1B13 /* Mastodon in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -181,10 +185,12 @@
|
||||||
D0C7D46824F76169001EBDBB /* Extensions */,
|
D0C7D46824F76169001EBDBB /* Extensions */,
|
||||||
D0666A7924C7745A00F3F04B /* Frameworks */,
|
D0666A7924C7745A00F3F04B /* Frameworks */,
|
||||||
D0BFDAF524FC7C5300C86618 /* HTTP */,
|
D0BFDAF524FC7C5300C86618 /* HTTP */,
|
||||||
|
D0BECB952501B3DD002C1B13 /* Keychain */,
|
||||||
D0C7D45624F76169001EBDBB /* Localizations */,
|
D0C7D45624F76169001EBDBB /* Localizations */,
|
||||||
D0E0F1E424FC49FC002C04BF /* Mastodon */,
|
D0E0F1E424FC49FC002C04BF /* Mastodon */,
|
||||||
D0E5361A24E3EB4D00FB1CE1 /* Notification Service Extension */,
|
D0E5361A24E3EB4D00FB1CE1 /* Notification Service Extension */,
|
||||||
D047FA8D24C3E21200AF17C5 /* Products */,
|
D047FA8D24C3E21200AF17C5 /* Products */,
|
||||||
|
D0BECB962501BCE0002C1B13 /* Secrets */,
|
||||||
D0BDF66524FD7A6400C7FA1C /* ServiceLayer */,
|
D0BDF66524FD7A6400C7FA1C /* ServiceLayer */,
|
||||||
D0C7D41D24F76169001EBDBB /* Supporting Files */,
|
D0C7D41D24F76169001EBDBB /* Supporting Files */,
|
||||||
D0C7D45324F76169001EBDBB /* System */,
|
D0C7D45324F76169001EBDBB /* System */,
|
||||||
|
@ -323,7 +329,7 @@
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
D06B492224D4611300642749 /* KingfisherSwiftUI */,
|
D06B492224D4611300642749 /* KingfisherSwiftUI */,
|
||||||
D0E2C1D024FD97F000854680 /* ViewModels */,
|
D0E2C1D024FD97F000854680 /* ViewModels */,
|
||||||
D0175CAB24FE2D6300B085F6 /* PreviewViewModels */,
|
D0BECB9B2501C731002C1B13 /* PreviewViewModels */,
|
||||||
);
|
);
|
||||||
productName = "Metatext (iOS)";
|
productName = "Metatext (iOS)";
|
||||||
productReference = D047FA8C24C3E21200AF17C5 /* Metatext.app */;
|
productReference = D047FA8C24C3E21200AF17C5 /* Metatext.app */;
|
||||||
|
@ -363,7 +369,8 @@
|
||||||
);
|
);
|
||||||
name = "Notification Service Extension";
|
name = "Notification Service Extension";
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
D0BDF66A24FD7CEC00C7FA1C /* ServiceLayer */,
|
D0BECB972501C0FC002C1B13 /* Secrets */,
|
||||||
|
D0BECB992501C15F002C1B13 /* Mastodon */,
|
||||||
);
|
);
|
||||||
productName = "Notification Service Extension";
|
productName = "Notification Service Extension";
|
||||||
productReference = D0E5361924E3EB4D00FB1CE1 /* Notification Service Extension.appex */;
|
productReference = D0E5361924E3EB4D00FB1CE1 /* Notification Service Extension.appex */;
|
||||||
|
@ -841,18 +848,22 @@
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
D0175CAB24FE2D6300B085F6 /* PreviewViewModels */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
productName = PreviewViewModels;
|
|
||||||
};
|
|
||||||
D06B492224D4611300642749 /* KingfisherSwiftUI */ = {
|
D06B492224D4611300642749 /* KingfisherSwiftUI */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = D06B492124D4611300642749 /* XCRemoteSwiftPackageReference "Kingfisher" */;
|
package = D06B492124D4611300642749 /* XCRemoteSwiftPackageReference "Kingfisher" */;
|
||||||
productName = KingfisherSwiftUI;
|
productName = KingfisherSwiftUI;
|
||||||
};
|
};
|
||||||
D0BDF66A24FD7CEC00C7FA1C /* ServiceLayer */ = {
|
D0BECB972501C0FC002C1B13 /* Secrets */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = ServiceLayer;
|
productName = Secrets;
|
||||||
|
};
|
||||||
|
D0BECB992501C15F002C1B13 /* Mastodon */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = Mastodon;
|
||||||
|
};
|
||||||
|
D0BECB9B2501C731002C1B13 /* PreviewViewModels */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = PreviewViewModels;
|
||||||
};
|
};
|
||||||
D0E2C1D024FD97F000854680 /* ViewModels */ = {
|
D0E2C1D024FD97F000854680 /* ViewModels */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
|
|
@ -2,8 +2,9 @@
|
||||||
|
|
||||||
import UserNotifications
|
import UserNotifications
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
|
import Keychain
|
||||||
import Mastodon
|
import Mastodon
|
||||||
import ServiceLayer
|
import Secrets
|
||||||
|
|
||||||
class NotificationService: UNNotificationServiceExtension {
|
class NotificationService: UNNotificationServiceExtension {
|
||||||
|
|
||||||
|
@ -91,7 +92,7 @@ private extension NotificationService {
|
||||||
let serverPublicKeyData = Data(base64Encoded: serverPublicKeyBase64)
|
let serverPublicKeyData = Data(base64Encoded: serverPublicKeyBase64)
|
||||||
else { throw NotificationServiceError.userInfoDataAbsent }
|
else { throw NotificationServiceError.userInfoDataAbsent }
|
||||||
|
|
||||||
let secretsService = SecretsService(identityID: identityID, keychainService: LiveKeychainService.self)
|
let secretsService = Secrets(identityID: identityID, keychain: LiveKeychain.self)
|
||||||
|
|
||||||
guard
|
guard
|
||||||
let auth = try secretsService.getPushAuth(),
|
let auth = try secretsService.getPushAuth(),
|
||||||
|
|
5
Secrets/.gitignore
vendored
Normal file
5
Secrets/.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
.DS_Store
|
||||||
|
/.build
|
||||||
|
/Packages
|
||||||
|
/*.xcodeproj
|
||||||
|
xcuserdata/
|
27
Secrets/Package.swift
Normal file
27
Secrets/Package.swift
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
// swift-tools-version:5.3
|
||||||
|
|
||||||
|
import PackageDescription
|
||||||
|
|
||||||
|
let package = Package(
|
||||||
|
name: "Secrets",
|
||||||
|
platforms: [
|
||||||
|
.iOS(.v14),
|
||||||
|
.macOS(.v11)
|
||||||
|
],
|
||||||
|
products: [
|
||||||
|
.library(
|
||||||
|
name: "Secrets",
|
||||||
|
targets: ["Secrets"])
|
||||||
|
],
|
||||||
|
dependencies: [
|
||||||
|
.package(path: "Keychain")
|
||||||
|
],
|
||||||
|
targets: [
|
||||||
|
.target(
|
||||||
|
name: "Secrets",
|
||||||
|
dependencies: ["Keychain"]),
|
||||||
|
.testTarget(
|
||||||
|
name: "SecretsTests",
|
||||||
|
dependencies: ["Secrets"])
|
||||||
|
]
|
||||||
|
)
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Keychain
|
||||||
|
|
||||||
public protocol SecretsStorable {
|
public protocol SecretsStorable {
|
||||||
var dataStoredInSecrets: Data { get }
|
var dataStoredInSecrets: Data { get }
|
||||||
|
@ -11,17 +12,17 @@ enum SecretsStorableError: Error {
|
||||||
case conversionFromDataStoredInSecrets(Data)
|
case conversionFromDataStoredInSecrets(Data)
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct SecretsService {
|
public struct Secrets {
|
||||||
public let identityID: UUID
|
public let identityID: UUID
|
||||||
private let keychainService: KeychainService.Type
|
private let keychain: Keychain.Type
|
||||||
|
|
||||||
public init(identityID: UUID, keychainService: KeychainService.Type) {
|
public init(identityID: UUID, keychain: Keychain.Type) {
|
||||||
self.identityID = identityID
|
self.identityID = identityID
|
||||||
self.keychainService = keychainService
|
self.keychain = keychain
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension SecretsService {
|
public extension Secrets {
|
||||||
enum Item: String, CaseIterable {
|
enum Item: String, CaseIterable {
|
||||||
case clientID
|
case clientID
|
||||||
case clientSecret
|
case clientSecret
|
||||||
|
@ -35,7 +36,7 @@ enum SecretsServiceError: Error {
|
||||||
case itemAbsent
|
case itemAbsent
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SecretsService.Item {
|
extension Secrets.Item {
|
||||||
enum Kind {
|
enum Kind {
|
||||||
case genericPassword
|
case genericPassword
|
||||||
case key
|
case key
|
||||||
|
@ -49,16 +50,16 @@ extension SecretsService.Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension SecretsService {
|
public extension Secrets {
|
||||||
func set(_ data: SecretsStorable, forItem item: Item) throws {
|
func set(_ data: SecretsStorable, forItem item: Item) throws {
|
||||||
try keychainService.setGenericPassword(
|
try keychain.setGenericPassword(
|
||||||
data: data.dataStoredInSecrets,
|
data: data.dataStoredInSecrets,
|
||||||
forAccount: key(item: item),
|
forAccount: key(item: item),
|
||||||
service: Self.keychainServiceName)
|
service: Self.keychainServiceName)
|
||||||
}
|
}
|
||||||
|
|
||||||
func item<T: SecretsStorable>(_ item: Item) throws -> T {
|
func item<T: SecretsStorable>(_ item: Item) throws -> T {
|
||||||
guard let data = try keychainService.getGenericPassword(
|
guard let data = try keychain.getGenericPassword(
|
||||||
account: key(item: item),
|
account: key(item: item),
|
||||||
service: Self.keychainServiceName) else {
|
service: Self.keychainServiceName) else {
|
||||||
throw SecretsServiceError.itemAbsent
|
throw SecretsServiceError.itemAbsent
|
||||||
|
@ -68,26 +69,26 @@ public extension SecretsService {
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteAllItems() throws {
|
func deleteAllItems() throws {
|
||||||
for item in SecretsService.Item.allCases {
|
for item in Secrets.Item.allCases {
|
||||||
switch item.kind {
|
switch item.kind {
|
||||||
case .genericPassword:
|
case .genericPassword:
|
||||||
try keychainService.deleteGenericPassword(
|
try keychain.deleteGenericPassword(
|
||||||
account: key(item: item),
|
account: key(item: item),
|
||||||
service: Self.keychainServiceName)
|
service: Self.keychainServiceName)
|
||||||
case .key:
|
case .key:
|
||||||
try keychainService.deleteKey(applicationTag: key(item: item))
|
try keychain.deleteKey(applicationTag: key(item: item))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func generatePushKeyAndReturnPublicKey() throws -> Data {
|
func generatePushKeyAndReturnPublicKey() throws -> Data {
|
||||||
try keychainService.generateKeyAndReturnPublicKey(
|
try keychain.generateKeyAndReturnPublicKey(
|
||||||
applicationTag: key(item: .pushKey),
|
applicationTag: key(item: .pushKey),
|
||||||
attributes: PushKey.attributes)
|
attributes: PushKey.attributes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPushKey() throws -> Data? {
|
func getPushKey() throws -> Data? {
|
||||||
try keychainService.getPrivateKey(
|
try keychain.getPrivateKey(
|
||||||
applicationTag: key(item: .pushKey),
|
applicationTag: key(item: .pushKey),
|
||||||
attributes: PushKey.attributes)
|
attributes: PushKey.attributes)
|
||||||
}
|
}
|
||||||
|
@ -109,7 +110,7 @@ public extension SecretsService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension SecretsService {
|
private extension Secrets {
|
||||||
static let keychainServiceName = "com.metabolist.metatext"
|
static let keychainServiceName = "com.metabolist.metatext"
|
||||||
|
|
||||||
func key(item: Item) -> String {
|
func key(item: Item) -> String {
|
10
Secrets/Tests/SecretsTests/SecretsTests.swift
Normal file
10
Secrets/Tests/SecretsTests/SecretsTests.swift
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import XCTest
|
||||||
|
@testable import Secrets
|
||||||
|
|
||||||
|
final class SecretsTests: XCTestCase {
|
||||||
|
func testExample() {
|
||||||
|
// This is an example of a functional test case.
|
||||||
|
// Use XCTAssert and related functions to verify your tests produce the correct
|
||||||
|
// results.
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,15 +19,20 @@ let package = Package(
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(url: "https://github.com/groue/CombineExpectations.git", .upToNextMajor(from: "0.5.0")),
|
.package(url: "https://github.com/groue/CombineExpectations.git", .upToNextMajor(from: "0.5.0")),
|
||||||
.package(path: "DB"),
|
.package(path: "DB"),
|
||||||
.package(path: "Mastodon")
|
.package(path: "Keychain"),
|
||||||
|
.package(path: "Mastodon"),
|
||||||
|
.package(path: "Secrets")
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.target(
|
.target(
|
||||||
name: "ServiceLayer",
|
name: "ServiceLayer",
|
||||||
dependencies: ["DB"]),
|
dependencies: ["DB", "Secrets"]),
|
||||||
.target(
|
.target(
|
||||||
name: "ServiceLayerMocks",
|
name: "ServiceLayerMocks",
|
||||||
dependencies: ["ServiceLayer", .product(name: "MastodonStubs", package: "Mastodon")]),
|
dependencies: [
|
||||||
|
"ServiceLayer",
|
||||||
|
.product(name: "MastodonStubs", package: "Mastodon"),
|
||||||
|
.product(name: "MockKeychain", package: "Keychain")]),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "ServiceLayerTests",
|
name: "ServiceLayerTests",
|
||||||
dependencies: ["CombineExpectations", "ServiceLayerMocks"])
|
dependencies: ["CombineExpectations", "ServiceLayerMocks"])
|
||||||
|
|
|
@ -4,6 +4,7 @@ import DB
|
||||||
import Foundation
|
import Foundation
|
||||||
import Combine
|
import Combine
|
||||||
import Mastodon
|
import Mastodon
|
||||||
|
import Secrets
|
||||||
|
|
||||||
public struct AllIdentitiesService {
|
public struct AllIdentitiesService {
|
||||||
public let mostRecentlyUsedIdentityID: AnyPublisher<UUID?, Never>
|
public let mostRecentlyUsedIdentityID: AnyPublisher<UUID?, Never>
|
||||||
|
@ -34,24 +35,24 @@ public extension AllIdentitiesService {
|
||||||
}
|
}
|
||||||
|
|
||||||
func authorizeIdentity(id: UUID, instanceURL: URL) -> AnyPublisher<Never, Error> {
|
func authorizeIdentity(id: UUID, instanceURL: URL) -> AnyPublisher<Never, Error> {
|
||||||
let secretsService = SecretsService(identityID: id, keychainService: environment.keychainServiceType)
|
let secrets = Secrets(identityID: id, keychain: environment.keychain)
|
||||||
let authenticationService = AuthenticationService(environment: environment)
|
let authenticationService = AuthenticationService(environment: environment)
|
||||||
|
|
||||||
return authenticationService.authorizeApp(instanceURL: instanceURL)
|
return authenticationService.authorizeApp(instanceURL: instanceURL)
|
||||||
.tryMap { appAuthorization -> (URL, AppAuthorization) in
|
.tryMap { appAuthorization -> (URL, AppAuthorization) in
|
||||||
try secretsService.set(appAuthorization.clientId, forItem: .clientID)
|
try secrets.set(appAuthorization.clientId, forItem: .clientID)
|
||||||
try secretsService.set(appAuthorization.clientSecret, forItem: .clientSecret)
|
try secrets.set(appAuthorization.clientSecret, forItem: .clientSecret)
|
||||||
|
|
||||||
return (instanceURL, appAuthorization)
|
return (instanceURL, appAuthorization)
|
||||||
}
|
}
|
||||||
.flatMap(authenticationService.authenticate(instanceURL:appAuthorization:))
|
.flatMap(authenticationService.authenticate(instanceURL:appAuthorization:))
|
||||||
.tryMap { try secretsService.set($0.accessToken, forItem: .accessToken) }
|
.tryMap { try secrets.set($0.accessToken, forItem: .accessToken) }
|
||||||
.ignoreOutput()
|
.ignoreOutput()
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteIdentity(_ identity: Identity) -> AnyPublisher<Never, Error> {
|
func deleteIdentity(_ identity: Identity) -> AnyPublisher<Never, Error> {
|
||||||
let secretsService = SecretsService(identityID: identity.id, keychainService: environment.keychainServiceType)
|
let secrets = Secrets(identityID: identity.id, keychain: environment.keychain)
|
||||||
let networkClient = APIClient(session: environment.session)
|
let networkClient = APIClient(session: environment.session)
|
||||||
|
|
||||||
networkClient.instanceURL = identity.url
|
networkClient.instanceURL = identity.url
|
||||||
|
@ -60,14 +61,14 @@ public extension AllIdentitiesService {
|
||||||
.collect()
|
.collect()
|
||||||
.tryMap { _ in
|
.tryMap { _ in
|
||||||
DeletionEndpoint.oauthRevoke(
|
DeletionEndpoint.oauthRevoke(
|
||||||
token: try secretsService.item(.accessToken),
|
token: try secrets.item(.accessToken),
|
||||||
clientID: try secretsService.item(.clientID),
|
clientID: try secrets.item(.clientID),
|
||||||
clientSecret: try secretsService.item(.clientSecret))
|
clientSecret: try secrets.item(.clientSecret))
|
||||||
}
|
}
|
||||||
.flatMap(networkClient.request)
|
.flatMap(networkClient.request)
|
||||||
.collect()
|
.collect()
|
||||||
.tryMap { _ in
|
.tryMap { _ in
|
||||||
try secretsService.deleteAllItems()
|
try secrets.deleteAllItems()
|
||||||
try ContentDatabase.delete(forIdentityID: identity.id)
|
try ContentDatabase.delete(forIdentityID: identity.id)
|
||||||
}
|
}
|
||||||
.ignoreOutput()
|
.ignoreOutput()
|
||||||
|
|
|
@ -3,13 +3,14 @@
|
||||||
import DB
|
import DB
|
||||||
import Foundation
|
import Foundation
|
||||||
import HTTP
|
import HTTP
|
||||||
|
import Keychain
|
||||||
import Mastodon
|
import Mastodon
|
||||||
import UserNotifications
|
import UserNotifications
|
||||||
|
|
||||||
public struct AppEnvironment {
|
public struct AppEnvironment {
|
||||||
let session: Session
|
let session: Session
|
||||||
let webAuthSessionType: WebAuthSession.Type
|
let webAuthSessionType: WebAuthSession.Type
|
||||||
let keychainServiceType: KeychainService.Type
|
let keychain: Keychain.Type
|
||||||
let userDefaults: UserDefaults
|
let userDefaults: UserDefaults
|
||||||
let userNotificationClient: UserNotificationClient
|
let userNotificationClient: UserNotificationClient
|
||||||
let inMemoryContent: Bool
|
let inMemoryContent: Bool
|
||||||
|
@ -17,14 +18,14 @@ public struct AppEnvironment {
|
||||||
|
|
||||||
public init(session: Session,
|
public init(session: Session,
|
||||||
webAuthSessionType: WebAuthSession.Type,
|
webAuthSessionType: WebAuthSession.Type,
|
||||||
keychainServiceType: KeychainService.Type,
|
keychain: Keychain.Type,
|
||||||
userDefaults: UserDefaults,
|
userDefaults: UserDefaults,
|
||||||
userNotificationClient: UserNotificationClient,
|
userNotificationClient: UserNotificationClient,
|
||||||
inMemoryContent: Bool,
|
inMemoryContent: Bool,
|
||||||
identityFixture: IdentityFixture?) {
|
identityFixture: IdentityFixture?) {
|
||||||
self.session = session
|
self.session = session
|
||||||
self.webAuthSessionType = webAuthSessionType
|
self.webAuthSessionType = webAuthSessionType
|
||||||
self.keychainServiceType = keychainServiceType
|
self.keychain = keychain
|
||||||
self.userDefaults = userDefaults
|
self.userDefaults = userDefaults
|
||||||
self.userNotificationClient = userNotificationClient
|
self.userNotificationClient = userNotificationClient
|
||||||
self.inMemoryContent = inMemoryContent
|
self.inMemoryContent = inMemoryContent
|
||||||
|
@ -37,7 +38,7 @@ public extension AppEnvironment {
|
||||||
Self(
|
Self(
|
||||||
session: Session(configuration: .default),
|
session: Session(configuration: .default),
|
||||||
webAuthSessionType: LiveWebAuthSession.self,
|
webAuthSessionType: LiveWebAuthSession.self,
|
||||||
keychainServiceType: LiveKeychainService.self,
|
keychain: LiveKeychain.self,
|
||||||
userDefaults: .standard,
|
userDefaults: .standard,
|
||||||
userNotificationClient: .live(userNotificationCenter),
|
userNotificationClient: .live(userNotificationCenter),
|
||||||
inMemoryContent: false,
|
inMemoryContent: false,
|
||||||
|
|
|
@ -4,6 +4,7 @@ import DB
|
||||||
import Foundation
|
import Foundation
|
||||||
import Combine
|
import Combine
|
||||||
import Mastodon
|
import Mastodon
|
||||||
|
import Secrets
|
||||||
|
|
||||||
public class IdentityService {
|
public class IdentityService {
|
||||||
@Published public private(set) var identity: Identity
|
@Published public private(set) var identity: Identity
|
||||||
|
@ -13,7 +14,7 @@ public class IdentityService {
|
||||||
private let contentDatabase: ContentDatabase
|
private let contentDatabase: ContentDatabase
|
||||||
private let environment: AppEnvironment
|
private let environment: AppEnvironment
|
||||||
private let networkClient: APIClient
|
private let networkClient: APIClient
|
||||||
private let secretsService: SecretsService
|
private let secrets: Secrets
|
||||||
private let observationErrorsInput = PassthroughSubject<Error, Never>()
|
private let observationErrorsInput = PassthroughSubject<Error, Never>()
|
||||||
|
|
||||||
init(identityID: UUID,
|
init(identityID: UUID,
|
||||||
|
@ -33,12 +34,12 @@ public class IdentityService {
|
||||||
guard let identity = initialIdentity else { throw IdentityDatabaseError.identityNotFound }
|
guard let identity = initialIdentity else { throw IdentityDatabaseError.identityNotFound }
|
||||||
|
|
||||||
self.identity = identity
|
self.identity = identity
|
||||||
secretsService = SecretsService(
|
secrets = Secrets(
|
||||||
identityID: identityID,
|
identityID: identityID,
|
||||||
keychainService: environment.keychainServiceType)
|
keychain: environment.keychain)
|
||||||
networkClient = APIClient(session: environment.session)
|
networkClient = APIClient(session: environment.session)
|
||||||
networkClient.instanceURL = identity.url
|
networkClient.instanceURL = identity.url
|
||||||
networkClient.accessToken = try? secretsService.item(.accessToken)
|
networkClient.accessToken = try? secrets.item(.accessToken)
|
||||||
|
|
||||||
contentDatabase = try ContentDatabase(identityID: identityID, inMemory: environment.inMemoryContent)
|
contentDatabase = try ContentDatabase(identityID: identityID, inMemory: environment.inMemoryContent)
|
||||||
|
|
||||||
|
@ -168,8 +169,8 @@ public extension IdentityService {
|
||||||
let auth: String
|
let auth: String
|
||||||
|
|
||||||
do {
|
do {
|
||||||
publicKey = try secretsService.generatePushKeyAndReturnPublicKey().base64EncodedString()
|
publicKey = try secrets.generatePushKeyAndReturnPublicKey().base64EncodedString()
|
||||||
auth = try secretsService.generatePushAuth().base64EncodedString()
|
auth = try secrets.generatePushAuth().base64EncodedString()
|
||||||
} catch {
|
} catch {
|
||||||
return Fail(error: error).eraseToAnyPublisher()
|
return Fail(error: error).eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
import DB
|
import DB
|
||||||
import Foundation
|
import Foundation
|
||||||
import HTTP
|
import HTTP
|
||||||
|
import MockKeychain
|
||||||
import ServiceLayer
|
import ServiceLayer
|
||||||
import Stubbing
|
import Stubbing
|
||||||
|
|
||||||
|
@ -11,7 +12,7 @@ public extension AppEnvironment {
|
||||||
AppEnvironment(
|
AppEnvironment(
|
||||||
session: Session(configuration: .stubbing),
|
session: Session(configuration: .stubbing),
|
||||||
webAuthSessionType: SuccessfulMockWebAuthSession.self,
|
webAuthSessionType: SuccessfulMockWebAuthSession.self,
|
||||||
keychainServiceType: MockKeychainService.self,
|
keychain: MockKeychain.self,
|
||||||
userDefaults: MockUserDefaults(),
|
userDefaults: MockUserDefaults(),
|
||||||
userNotificationClient: .mock,
|
userNotificationClient: .mock,
|
||||||
inMemoryContent: true,
|
inMemoryContent: true,
|
||||||
|
|
|
@ -5,6 +5,7 @@ import Combine
|
||||||
import CombineExpectations
|
import CombineExpectations
|
||||||
import HTTP
|
import HTTP
|
||||||
import Mastodon
|
import Mastodon
|
||||||
|
import MockKeychain
|
||||||
import ServiceLayer
|
import ServiceLayer
|
||||||
import ServiceLayerMocks
|
import ServiceLayerMocks
|
||||||
@testable import ViewModels
|
@testable import ViewModels
|
||||||
|
@ -48,7 +49,7 @@ class AddIdentityViewModelTests: XCTestCase {
|
||||||
let environment = AppEnvironment(
|
let environment = AppEnvironment(
|
||||||
session: Session(configuration: .stubbing),
|
session: Session(configuration: .stubbing),
|
||||||
webAuthSessionType: CanceledLoginMockWebAuthSession.self,
|
webAuthSessionType: CanceledLoginMockWebAuthSession.self,
|
||||||
keychainServiceType: MockKeychainService.self,
|
keychain: MockKeychain.self,
|
||||||
userDefaults: MockUserDefaults(),
|
userDefaults: MockUserDefaults(),
|
||||||
userNotificationClient: .mock,
|
userNotificationClient: .mock,
|
||||||
inMemoryContent: true,
|
inMemoryContent: true,
|
||||||
|
|
Loading…
Reference in a new issue