Refactoring

This commit is contained in:
Justin Mazzocchi 2020-09-03 23:44:04 -07:00
parent fb4e3f907f
commit f5cc39f256
No known key found for this signature in database
GPG key ID: E223E6937AAFB01C
4 changed files with 88 additions and 73 deletions

View file

@ -1,40 +0,0 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Foundation
import Keychain
import Secrets
extension Secrets {
private static let passphraseByteCount = 64
static func databasePassphrase(identityID: UUID?, keychain: Keychain.Type) throws -> String {
let scopedSecrets: Secrets?
if let identityID = identityID {
scopedSecrets = Secrets(identityID: identityID, keychain: keychain)
} else {
scopedSecrets = nil
}
do {
return try scopedSecrets?.item(.databasePassphrase) ?? unscopedItem(.databasePassphrase, keychain: keychain)
} catch SecretsError.itemAbsent {
var bytes = [Int8](repeating: 0, count: passphraseByteCount)
let status = SecRandomCopyBytes(kSecRandomDefault, passphraseByteCount, &bytes)
if status == errSecSuccess {
let passphrase = Data(bytes: bytes, count: passphraseByteCount).base64EncodedString()
if let scopedSecrets = scopedSecrets {
try scopedSecrets.set(passphrase, forItem: .databasePassphrase)
} else {
try setUnscoped(passphrase, forItem: .databasePassphrase, keychain: keychain)
}
return passphrase
} else {
throw NSError(status: status)
}
}
}
}

View file

@ -52,38 +52,35 @@ extension Secrets.Item {
} }
public extension Secrets { public extension Secrets {
static func setUnscoped(_ data: SecretsStorable, forItem item: Item, keychain: Keychain.Type) throws { static func databasePassphrase(identityID: UUID?, keychain: Keychain.Type) throws -> String {
try keychain.setGenericPassword( let scopedSecrets: Secrets?
data: data.dataStoredInSecrets,
forAccount: item.rawValue,
service: keychainServiceName)
}
static func unscopedItem<T: SecretsStorable>(_ item: Item, keychain: Keychain.Type) throws -> T { if let identityID = identityID {
guard let data = try keychain.getGenericPassword( scopedSecrets = Secrets(identityID: identityID, keychain: keychain)
account: item.rawValue, } else {
service: Self.keychainServiceName) else { scopedSecrets = nil
throw SecretsError.itemAbsent
} }
return try T.fromDataStoredInSecrets(data) do {
} return try scopedSecrets?.item(.databasePassphrase) ?? unscopedItem(.databasePassphrase, keychain: keychain)
} catch SecretsError.itemAbsent {
var bytes = [Int8](repeating: 0, count: databasePassphraseByteCount)
let status = SecRandomCopyBytes(kSecRandomDefault, databasePassphraseByteCount, &bytes)
func set(_ data: SecretsStorable, forItem item: Item) throws { if status == errSecSuccess {
try keychain.setGenericPassword( let passphrase = Data(bytes: bytes, count: databasePassphraseByteCount).base64EncodedString()
data: data.dataStoredInSecrets,
forAccount: scopedKey(item: item),
service: Self.keychainServiceName)
}
func item<T: SecretsStorable>(_ item: Item) throws -> T { if let scopedSecrets = scopedSecrets {
guard let data = try keychain.getGenericPassword( try scopedSecrets.set(passphrase, forItem: .databasePassphrase)
account: scopedKey(item: item), } else {
service: Self.keychainServiceName) else { try setUnscoped(passphrase, forItem: .databasePassphrase, keychain: keychain)
throw SecretsError.itemAbsent }
return passphrase
} else {
throw NSError(status: status)
}
} }
return try T.fromDataStoredInSecrets(data)
} }
func deleteAllItems() throws { func deleteAllItems() throws {
@ -99,6 +96,30 @@ public extension Secrets {
} }
} }
func getClientID() throws -> String {
try item(.clientID)
}
func setClientID(_ clientID: String) throws {
try set(clientID, forItem: .clientID)
}
func getClientSecret() throws -> String {
try item(.clientSecret)
}
func setClientSecret(_ clientSecret: String) throws {
try set(clientSecret, forItem: .clientSecret)
}
func getAccessToken() throws -> String {
try item(.accessToken)
}
func setAccessToken(_ accessToken: String) throws {
try set(accessToken, forItem: .accessToken)
}
func generatePushKeyAndReturnPublicKey() throws -> Data { func generatePushKeyAndReturnPublicKey() throws -> Data {
try keychain.generateKeyAndReturnPublicKey( try keychain.generateKeyAndReturnPublicKey(
applicationTag: scopedKey(item: .pushKey), applicationTag: scopedKey(item: .pushKey),
@ -130,10 +151,44 @@ public extension Secrets {
private extension Secrets { private extension Secrets {
static let keychainServiceName = "com.metabolist.metatext" static let keychainServiceName = "com.metabolist.metatext"
static let databasePassphraseByteCount = 64
private static func set(_ data: SecretsStorable, forAccount account: String, keychain: Keychain.Type) throws {
try keychain.setGenericPassword(
data: data.dataStoredInSecrets,
forAccount: account,
service: keychainServiceName)
}
private static func get<T: SecretsStorable>(account: String, keychain: Keychain.Type) throws -> T {
guard let data = try keychain.getGenericPassword(
account: account,
service: keychainServiceName) else {
throw SecretsError.itemAbsent
}
return try T.fromDataStoredInSecrets(data)
}
static func setUnscoped(_ data: SecretsStorable, forItem item: Item, keychain: Keychain.Type) throws {
try set(data, forAccount: item.rawValue, keychain: keychain)
}
static func unscopedItem<T: SecretsStorable>(_ item: Item, keychain: Keychain.Type) throws -> T {
try get(account: item.rawValue, keychain: keychain)
}
func scopedKey(item: Item) -> String { func scopedKey(item: Item) -> String {
identityID.uuidString + "." + item.rawValue identityID.uuidString + "." + item.rawValue
} }
func set(_ data: SecretsStorable, forItem item: Item) throws {
try Self.set(data, forAccount: scopedKey(item: item), keychain: keychain)
}
func item<T: SecretsStorable>(_ item: Item) throws -> T {
try Self.get(account: scopedKey(item: item), keychain: keychain)
}
} }
extension Data: SecretsStorable { extension Data: SecretsStorable {

View file

@ -42,13 +42,13 @@ public extension AllIdentitiesService {
return authenticationService.authorizeApp(instanceURL: instanceURL) return authenticationService.authorizeApp(instanceURL: instanceURL)
.tryMap { appAuthorization -> (URL, AppAuthorization) in .tryMap { appAuthorization -> (URL, AppAuthorization) in
try secrets.set(appAuthorization.clientId, forItem: .clientID) try secrets.setClientID(appAuthorization.clientId)
try secrets.set(appAuthorization.clientSecret, forItem: .clientSecret) try secrets.setClientSecret(appAuthorization.clientSecret)
return (instanceURL, appAuthorization) return (instanceURL, appAuthorization)
} }
.flatMap(authenticationService.authenticate(instanceURL:appAuthorization:)) .flatMap(authenticationService.authenticate(instanceURL:appAuthorization:))
.tryMap { try secrets.set($0.accessToken, forItem: .accessToken) } .tryMap { try secrets.setAccessToken($0.accessToken) }
.ignoreOutput() .ignoreOutput()
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
@ -63,9 +63,9 @@ public extension AllIdentitiesService {
.collect() .collect()
.tryMap { _ in .tryMap { _ in
DeletionEndpoint.oauthRevoke( DeletionEndpoint.oauthRevoke(
token: try secrets.item(.accessToken), token: try secrets.getAccessToken(),
clientID: try secrets.item(.clientID), clientID: try secrets.getClientID(),
clientSecret: try secrets.item(.clientSecret)) clientSecret: try secrets.getClientSecret())
} }
.flatMap(mastodonAPIClient.request) .flatMap(mastodonAPIClient.request)
.collect() .collect()

View file

@ -40,7 +40,7 @@ public class IdentityService {
keychain: environment.keychain) keychain: environment.keychain)
mastodonAPIClient = MastodonAPIClient(session: environment.session) mastodonAPIClient = MastodonAPIClient(session: environment.session)
mastodonAPIClient.instanceURL = identity.url mastodonAPIClient.instanceURL = identity.url
mastodonAPIClient.accessToken = try? secrets.item(.accessToken) mastodonAPIClient.accessToken = try? secrets.getAccessToken()
contentDatabase = try ContentDatabase(identityID: identityID, contentDatabase = try ContentDatabase(identityID: identityID,
inMemory: environment.inMemoryContent, inMemory: environment.inMemoryContent,