2020-07-29 23:50:30 +00:00
|
|
|
// 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-07-29 23:50:30 +00:00
|
|
|
|
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-07-29 23:50:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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
|
2020-07-29 23:50:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-14 03:40:46 +00:00
|
|
|
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)
|
2020-07-29 23:50:30 +00:00
|
|
|
}
|
|
|
|
|
2020-08-14 03:40:46 +00:00
|
|
|
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 {
|
2020-08-14 03:40:46 +00:00
|
|
|
throw SecretsServiceError.itemAbsent
|
2020-08-09 01:29:05 +00:00
|
|
|
}
|
2020-07-29 23:50:30 +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-07-29 23:50:30 +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(
|
2020-08-13 10:18:21 +00:00
|
|
|
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(
|
2020-08-13 10:18:21 +00:00
|
|
|
applicationTag: key(item: .pushKey),
|
|
|
|
attributes: PushKey.attributes)
|
2020-08-12 07:24:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func generatePushAuth() throws -> Data {
|
2020-08-13 10:18:21 +00:00
|
|
|
var bytes = [UInt8](repeating: 0, count: PushKey.authLength)
|
2020-08-12 07:24:39 +00:00
|
|
|
|
2020-08-13 10:18:21 +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-07-29 23:50:30 +00:00
|
|
|
}
|
|
|
|
|
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 {
|
2020-08-07 23:19:13 +00:00
|
|
|
identityID.uuidString + "." + item.rawValue
|
2020-07-29 23:50:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
2020-08-13 10:18:21 +00:00
|
|
|
|
|
|
|
struct PushKey {
|
|
|
|
static let authLength = 16
|
|
|
|
static let sizeInBits = 256
|
|
|
|
static let attributes: [String: Any] = [
|
|
|
|
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
|
|
|
|
kSecAttrKeySizeInBits as String: sizeInBits]
|
|
|
|
}
|