metatext/Secrets/Sources/Secrets/Secrets.swift

299 lines
8.6 KiB
Swift
Raw Normal View History

// Copyright © 2020 Metabolist. All rights reserved.
2020-09-06 21:37:54 +00:00
import Base16
2021-01-30 01:14:22 +00:00
import CryptoKit
import Foundation
2020-09-04 00:54:05 +00:00
import Keychain
2020-08-31 10:21:01 +00:00
public protocol SecretsStorable {
var dataStoredInSecrets: Data { get }
static func fromDataStoredInSecrets(_ data: Data) throws -> Self
}
enum SecretsStorableError: Error {
case conversionFromDataStoredInSecrets(Data)
}
2020-09-04 00:54:05 +00:00
public struct Secrets {
2020-10-05 22:50:05 +00:00
public let identityId: UUID
2020-09-04 00:54:05 +00:00
private let keychain: Keychain.Type
2020-10-05 22:50:05 +00:00
public init(identityId: UUID, keychain: Keychain.Type) {
self.identityId = identityId
2020-09-04 00:54:05 +00:00
self.keychain = keychain
}
}
2020-09-04 00:54:05 +00:00
public extension Secrets {
2020-08-09 02:52:41 +00:00
enum Item: String, CaseIterable {
2020-09-08 02:12:38 +00:00
case instanceURL
2020-10-05 22:50:05 +00:00
case clientId
2020-08-12 07:24:39 +00:00
case clientSecret
case accessToken
case pushKey
case pushAuth
2020-09-04 09:44:25 +00:00
case databaseKey
2021-01-30 01:14:22 +00:00
case imageCacheKey
2021-01-16 21:46:07 +00:00
case identityDatabaseName
case accountId
case username
}
}
2020-09-09 05:40:49 +00:00
public enum SecretsError: Error {
case itemAbsent
}
2020-09-04 00:54:05 +00:00
extension Secrets.Item {
2020-08-14 01:59:17 +00:00
enum Kind {
case genericPassword
case key
}
2021-01-30 01:14:22 +00:00
// Note `databaseKey` and `imageCacheKey` are stored as generic passwords, not keys
2020-08-14 01:59:17 +00:00
var kind: Kind {
switch self {
case .pushKey: return .key
default: return .genericPassword
}
}
}
2020-09-04 00:54:05 +00:00
public extension Secrets {
2021-01-16 21:46:07 +00:00
static func identityDatabaseName(keychain: Keychain.Type) throws -> String {
do {
return try unscopedItem(.identityDatabaseName, keychain: keychain)
} catch SecretsError.itemAbsent {
let identityDatabaseName = UUID().uuidString
try setUnscoped(identityDatabaseName, forItem: .identityDatabaseName, keychain: keychain)
return identityDatabaseName
}
}
2021-01-30 01:14:22 +00:00
static func imageCacheKey(keychain: Keychain.Type) throws -> Data {
do {
return try unscopedItem(.imageCacheKey, keychain: keychain)
} catch SecretsError.itemAbsent {
let imageCacheKey = Data(SymmetricKey(size: .bits256).withUnsafeBytes(Array.init))
try setUnscoped(imageCacheKey, forItem: .imageCacheKey, keychain: keychain)
return imageCacheKey
}
}
2020-09-04 09:44:25 +00:00
// https://www.zetetic.net/sqlcipher/sqlcipher-api/#key
2020-10-05 22:50:05 +00:00
static func databaseKey(identityId: UUID?, keychain: Keychain.Type) throws -> String {
2020-09-04 09:44:25 +00:00
let passphraseData: Data
2020-09-04 06:44:04 +00:00
let scopedSecrets: Secrets?
2020-09-04 06:12:06 +00:00
2020-10-05 22:50:05 +00:00
if let identityId = identityId {
scopedSecrets = Secrets(identityId: identityId, keychain: keychain)
2020-09-04 06:44:04 +00:00
} else {
scopedSecrets = nil
2020-09-04 06:12:06 +00:00
}
2020-09-04 06:44:04 +00:00
do {
2020-09-04 09:44:25 +00:00
passphraseData = try scopedSecrets?.item(.databaseKey)
?? unscopedItem(.databaseKey, keychain: keychain)
2020-09-04 06:44:04 +00:00
} catch SecretsError.itemAbsent {
2020-09-04 09:44:25 +00:00
var bytes = [UInt8](repeating: 0, count: databaseKeyLength)
let status = SecRandomCopyBytes(kSecRandomDefault, databaseKeyLength, &bytes)
2020-09-04 06:12:06 +00:00
2020-09-04 06:44:04 +00:00
if status == errSecSuccess {
2020-09-04 09:44:25 +00:00
passphraseData = Data(bytes)
2020-09-04 06:44:04 +00:00
if let scopedSecrets = scopedSecrets {
2020-09-04 09:44:25 +00:00
try scopedSecrets.set(passphraseData, forItem: .databaseKey)
2020-09-04 06:44:04 +00:00
} else {
2020-09-04 09:44:25 +00:00
try setUnscoped(passphraseData, forItem: .databaseKey, keychain: keychain)
2020-09-04 06:44:04 +00:00
}
} else {
throw NSError(status: status)
}
}
2020-09-04 09:44:25 +00:00
2020-09-06 21:37:54 +00:00
return "x'\(passphraseData.base16EncodedString(options: [.uppercase]))'"
}
2020-09-09 05:40:49 +00:00
func deleteAllItems() {
2020-09-04 00:54:05 +00:00
for item in Secrets.Item.allCases {
2020-09-09 05:40:49 +00:00
do {
switch item.kind {
case .genericPassword:
try keychain.deleteGenericPassword(
account: scopedKey(item: item),
service: Self.keychainServiceName)
case .key:
try keychain.deleteKey(applicationTag: scopedKey(item: item))
}
} catch {
// no-op
2020-08-14 01:59:17 +00:00
}
2020-08-09 02:52:41 +00:00
}
}
2020-08-12 07:24:39 +00:00
2020-09-08 02:12:38 +00:00
func getInstanceURL() throws -> URL {
try item(.instanceURL)
}
func setInstanceURL(_ instanceURL: URL) throws {
try set(instanceURL, forItem: .instanceURL)
}
2020-10-05 22:50:05 +00:00
func getClientId() throws -> String {
try item(.clientId)
2020-09-04 06:44:04 +00:00
}
2020-10-05 22:50:05 +00:00
func setClientId(_ clientId: String) throws {
try set(clientId, forItem: .clientId)
2020-09-04 06:44:04 +00:00
}
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 getAccountId() throws -> String {
try item(.accountId)
}
func setAccountId(_ accountId: String) throws {
try set(accountId, forItem: .accountId)
}
func getUsername() throws -> String {
try item(.username)
}
func setUsername(_ username: String) throws {
try set(username, forItem: .username)
}
2020-08-12 07:24:39 +00:00
func generatePushKeyAndReturnPublicKey() throws -> Data {
2020-09-04 00:54:05 +00:00
try keychain.generateKeyAndReturnPublicKey(
2020-09-04 06:12:06 +00:00
applicationTag: scopedKey(item: .pushKey),
attributes: PushKey.attributes)
2020-08-12 07:24:39 +00:00
}
func getPushKey() throws -> Data? {
2020-09-04 00:54:05 +00:00
try keychain.getPrivateKey(
2020-09-04 06:12:06 +00:00
applicationTag: scopedKey(item: .pushKey),
attributes: PushKey.attributes)
2020-08-12 07:24:39 +00:00
}
func generatePushAuth() throws -> Data {
var bytes = [UInt8](repeating: 0, count: PushKey.authLength)
2020-09-04 09:44:25 +00:00
let status = SecRandomCopyBytes(kSecRandomDefault, PushKey.authLength, &bytes)
2020-08-12 07:24:39 +00:00
2020-09-04 09:44:25 +00:00
if status == errSecSuccess {
let pushAuth = Data(bytes)
2020-08-12 07:24:39 +00:00
2020-09-04 09:44:25 +00:00
try set(pushAuth, forItem: .pushAuth)
2020-08-12 07:24:39 +00:00
2020-09-04 09:44:25 +00:00
return pushAuth
} else {
throw NSError(status: status)
}
2020-08-12 07:24:39 +00:00
}
func getPushAuth() throws -> Data? {
try item(.pushAuth)
}
}
2020-09-04 00:54:05 +00:00
private extension Secrets {
2020-08-12 07:24:39 +00:00
static let keychainServiceName = "com.metabolist.metatext"
2020-11-09 03:07:23 +00:00
static let databaseKeyLength = 48
2020-09-04 06:44:04 +00:00
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)
}
2020-08-12 07:24:39 +00:00
2020-09-04 06:12:06 +00:00
func scopedKey(item: Item) -> String {
2020-12-03 22:40:33 +00:00
identityId.uuidString.appending(".").appending(item.rawValue)
}
2020-09-04 06:44:04 +00:00
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 {
2020-08-31 10:21:01 +00:00
public var dataStoredInSecrets: Data { self }
2020-08-31 10:21:01 +00:00
public static func fromDataStoredInSecrets(_ data: Data) throws -> Data {
data
}
}
extension String: SecretsStorable {
2020-08-31 10:21:01 +00:00
public var dataStoredInSecrets: Data { Data(utf8) }
2020-08-31 10:21:01 +00:00
public static func fromDataStoredInSecrets(_ data: Data) throws -> String {
guard let string = String(data: data, encoding: .utf8) else {
throw SecretsStorableError.conversionFromDataStoredInSecrets(data)
}
return string
}
}
2020-09-08 02:12:38 +00:00
extension URL: SecretsStorable {
public var dataStoredInSecrets: Data { absoluteString.dataStoredInSecrets }
public static func fromDataStoredInSecrets(_ data: Data) throws -> URL {
guard let url = URL(string: try String.fromDataStoredInSecrets(data)) else {
throw SecretsStorableError.conversionFromDataStoredInSecrets(data)
}
return url
}
}
2020-08-31 10:21:01 +00:00
private struct PushKey {
static let authLength = 16
static let sizeInBits = 256
static let attributes: [String: Any] = [
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
kSecAttrKeySizeInBits as String: sizeInBits]
}