-
Notifications
You must be signed in to change notification settings - Fork 4
/
SecureArchiver.swift
156 lines (139 loc) · 5.18 KB
/
SecureArchiver.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
//
// SecureArchiver.swift
// ADUtils
//
// Created by Thomas Esterlin on 02/07/2021.
// Methods inspired by these : "https://fred.appelman.net/?p=119", "https://developer.apple.com/videos/play/wwdc2019/709"
import Foundation
import CryptoKit
public protocol KeychainArchiver {
func getValue(forKey: String) -> String?
func set(value: String?, forKey: String)
}
public protocol StorageArchiver {
func getValue(forKey key: String) -> Data?
func set(_ data: Data, forKey key: String)
func deleteValue(forKey key: String)
}
public class SecureArchiver {
private enum Constants {
static let passphrasePrefix = "cryptoKeyPassphrase_"
}
private enum SecureArchiverError: Error {
case invalidSymmetricKey
case unableToGenerateRandomKey
}
private let keychainArchiver: KeychainArchiver
private let storageArchiver: StorageArchiver
private let passphraseKey: String
private var cryptoKey = SymmetricKey(size: .bits256)
public init(keychainArchiver: KeychainArchiver,
storageArchiver: StorageArchiver,
appKey: String) {
self.keychainArchiver = keychainArchiver
self.storageArchiver = storageArchiver
passphraseKey = "\(Constants.passphrasePrefix)\(appKey)"
}
// MARK: - Public
/**
Set a value (encrypted) for a specific key in the user defaults.
- parameter value: The object to store in the user defaults.
- parameter key: The key to use in the user defaults.
*/
public func set<C: Codable>(_ value: C, forKey key: String) throws {
do {
let encoder = JSONEncoder()
let valueData = try encoder.encode(value)
let encryptedData = try ChaChaPoly.seal(valueData, using: getCryptoKey())
let dataToStore = encryptedData.combined
storageArchiver.set(dataToStore, forKey: key)
} catch {
throw error
}
}
/**
Read the value for a specific key in the user defaults.
- parameter key: The key to read in the user defaults.
- returns: The value associated to the key
*/
public func value<C: Codable>(forKey key: String) throws -> C? {
let optionalStoredValue: Data? = storageArchiver.getValue(forKey: key)
if let storedData = optionalStoredValue {
do {
let box = try ChaChaPoly.SealedBox(combined: storedData)
let decryptedData = try ChaChaPoly.open(box, using: getCryptoKey())
let decoder = JSONDecoder()
let object = try decoder.decode(C.self, from: decryptedData)
return object
} catch {
throw error
}
}
return nil
}
/**
Delete a value for a specific key in the storage archiver.
- parameter key: The key to read in the storage archiver.
*/
public func deleteValue(forKey key: String) {
storageArchiver.deleteValue(forKey: key)
}
// MARK: - Private
/**
Get the cryptographic key from the Keychain Archiver.
- returns: A symmetric key computed from the secret passphrase stored in KA.
*/
private func getCryptoKey() throws -> SymmetricKey {
if let storedPassphrase = keychainArchiver.getValue(forKey: passphraseKey) {
do {
return try keyFromPassphrase(storedPassphrase)
} catch {
throw error
}
}
do {
let passphrase = try generateRandomBytes(64)
keychainArchiver.set(value: passphrase, forKey: passphraseKey)
let newKey = try keyFromPassphrase(passphrase)
self.cryptoKey = newKey
return newKey
} catch {
throw error
}
}
/**
Create an encryption key from a given passphrase.
- parameter passphrase: The passphrase to compute the key.
- returns: The symmetric key computed from the passphrase.
*/
func keyFromPassphrase(_ passphrase: String) throws -> SymmetricKey {
// Create a SHA256 hash from the provided passphrase
if let passphraseData = passphrase.data(using: .utf8) {
let hash = SHA256.hash(data: passphraseData)
// Create the key use keyData as the seed
return SymmetricKey(data: hash)
}
throw SecureArchiverError.invalidSymmetricKey
}
/**
Create a random string using `SecRandomCopyBytes` according to OWASP.
https://github.com/OWASP/owasp-mstg/blob/master/Document/0x06e-Testing-Cryptography.md
- parameter length: The length of the string.
- returns: A random string .
*/
func generateRandomBytes(_ length: Int) throws -> String {
var keyData = Data(count: length)
let result = keyData.withUnsafeMutableBytes { bytes -> Int32 in
if let bytesAddress = bytes.baseAddress {
return SecRandomCopyBytes(kSecRandomDefault, length, bytesAddress)
} else {
return errSecMemoryError
}
}
if result == errSecSuccess {
return keyData.base64EncodedString()
} else {
throw SecureArchiverError.unableToGenerateRandomKey
}
}
}