metatext/Services/SecretsService.swift

147 lines
3.8 KiB
Swift
Raw Normal View History

// Copyright © 2020 Metabolist. All rights reserved.
import Foundation
protocol SecretsStorable {
var dataStoredInSecrets: Data { get }
static func fromDataStoredInSecrets(_ data: Data) throws -> Self
}
enum SecretsStorableError: Error {
case conversionFromDataStoredInSecrets(Data)
}
2020-08-09 02:52:41 +00:00
struct SecretsService {
let identityID: UUID
2020-08-14 01:59:17 +00:00
private let keychainService: KeychainService.Type
2020-08-14 01:59:17 +00:00
init(identityID: UUID, keychainService: KeychainService.Type) {
2020-08-09 02:52:41 +00:00
self.identityID = identityID
2020-08-14 01:59:17 +00:00
self.keychainService = keychainService
}
}
2020-08-09 02:52:41 +00:00
extension SecretsService {
enum Item: String, CaseIterable {
2020-08-12 07:24:39 +00:00
case clientID
case clientSecret
case accessToken
case pushKey
case pushAuth
}
}
enum SecretsServiceError: Error {
case itemAbsent
}
2020-08-14 01:59:17 +00:00
extension SecretsService.Item {
enum Kind {
case genericPassword
case key
}
var kind: Kind {
switch self {
case .pushKey: return .key
default: return .genericPassword
}
}
}
2020-08-09 02:52:41 +00:00
extension SecretsService {
func set(_ data: SecretsStorable, forItem item: Item) throws {
2020-08-14 01:59:17 +00:00
try keychainService.setGenericPassword(
2020-08-12 07:24:39 +00:00
data: data.dataStoredInSecrets,
forAccount: key(item: item),
service: Self.keychainServiceName)
}
func item<T: SecretsStorable>(_ item: Item) throws -> T {
2020-08-14 01:59:17 +00:00
guard let data = try keychainService.getGenericPassword(
2020-08-12 07:24:39 +00:00
account: key(item: item),
service: Self.keychainServiceName) else {
throw SecretsServiceError.itemAbsent
2020-08-09 01:29:05 +00:00
}
return try T.fromDataStoredInSecrets(data)
}
2020-08-09 02:52:41 +00:00
func deleteAllItems() throws {
for item in SecretsService.Item.allCases {
2020-08-14 01:59:17 +00:00
switch item.kind {
case .genericPassword:
try keychainService.deleteGenericPassword(
account: key(item: item),
service: Self.keychainServiceName)
case .key:
try keychainService.deleteKey(applicationTag: key(item: item))
}
2020-08-09 02:52:41 +00:00
}
}
2020-08-12 07:24:39 +00:00
func generatePushKeyAndReturnPublicKey() throws -> Data {
2020-08-14 01:59:17 +00:00
try keychainService.generateKeyAndReturnPublicKey(
applicationTag: key(item: .pushKey),
attributes: PushKey.attributes)
2020-08-12 07:24:39 +00:00
}
func getPushKey() throws -> Data? {
2020-08-14 01:59:17 +00:00
try keychainService.getPrivateKey(
applicationTag: key(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-08-12 07:24:39 +00:00
_ = SecRandomCopyBytes(kSecRandomDefault, PushKey.authLength, &bytes)
2020-08-12 07:24:39 +00:00
let pushAuth = Data(bytes)
try set(pushAuth, forItem: .pushAuth)
return pushAuth
}
func getPushAuth() throws -> Data? {
try item(.pushAuth)
}
}
2020-08-09 02:52:41 +00:00
private extension SecretsService {
2020-08-12 07:24:39 +00:00
static let keychainServiceName = "com.metabolist.metatext"
2020-08-09 02:52:41 +00:00
func key(item: Item) -> String {
identityID.uuidString + "." + item.rawValue
}
}
extension Data: SecretsStorable {
var dataStoredInSecrets: Data { self }
static func fromDataStoredInSecrets(_ data: Data) throws -> Data {
data
}
}
extension String: SecretsStorable {
var dataStoredInSecrets: Data { Data(utf8) }
static func fromDataStoredInSecrets(_ data: Data) throws -> String {
guard let string = String(data: data, encoding: .utf8) else {
throw SecretsStorableError.conversionFromDataStoredInSecrets(data)
}
return string
}
}
struct PushKey {
static let authLength = 16
static let sizeInBits = 256
static let attributes: [String: Any] = [
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
kSecAttrKeySizeInBits as String: sizeInBits]
}