Created
November 13, 2019 10:53
-
-
Save quangDecember/73ec18b6da125d175a2bace8b5ed419a to your computer and use it in GitHub Desktop.
property wrappers for encryption, using builtin Apple library
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import CommonCrypto | |
struct AES256 : Codable { | |
private var key: Data | |
private var iv: Data | |
public init(key: Data, iv: Data) throws { | |
guard key.count == kCCKeySizeAES256 else { | |
throw Error.badKeyLength | |
} | |
guard iv.count == kCCBlockSizeAES128 else { | |
throw Error.badInputVectorLength | |
} | |
self.key = key | |
self.iv = iv | |
} | |
enum Error: Swift.Error { | |
case keyGeneration(status: Int) | |
case cryptoFailed(status: CCCryptorStatus) | |
case badKeyLength | |
case badInputVectorLength | |
} | |
func encrypt(_ digest: Data) throws -> Data { | |
return try crypt(input: digest, operation: CCOperation(kCCEncrypt)) | |
} | |
func decrypt(_ encrypted: Data) throws -> Data { | |
return try crypt(input: encrypted, operation: CCOperation(kCCDecrypt)) | |
} | |
private func crypt(input: Data, operation: CCOperation) throws -> Data { | |
var outLength = Int(0) | |
var outBytes = [UInt8](repeating: 0, count: input.count + kCCBlockSizeAES128) | |
var status: CCCryptorStatus = CCCryptorStatus(kCCSuccess) | |
input.withUnsafeBytes { (encryptedBytes: UnsafePointer<UInt8>!) -> () in | |
iv.withUnsafeBytes { (ivBytes: UnsafePointer<UInt8>!) in | |
key.withUnsafeBytes { (keyBytes: UnsafePointer<UInt8>!) -> () in | |
status = CCCrypt(operation, | |
CCAlgorithm(kCCAlgorithmAES128), // algorithm | |
CCOptions(kCCOptionPKCS7Padding), // options | |
keyBytes, // key | |
key.count, // keylength | |
ivBytes, // iv | |
encryptedBytes, // dataIn | |
input.count, // dataInLength | |
&outBytes, // dataOut | |
outBytes.count, // dataOutAvailable | |
&outLength) // dataOutMoved | |
} | |
} | |
} | |
guard status == kCCSuccess else { | |
throw Error.cryptoFailed(status: status) | |
} | |
return Data(bytes: UnsafePointer<UInt8>(outBytes), count: outLength) | |
} | |
static func createKey(password: Data, salt: Data) throws -> Data { | |
let length = kCCKeySizeAES256 | |
var status = Int32(0) | |
var derivedBytes = [UInt8](repeating: 0, count: length) | |
password.withUnsafeBytes { (passwordBytes: UnsafePointer<Int8>!) in | |
salt.withUnsafeBytes { (saltBytes: UnsafePointer<UInt8>!) in | |
status = CCKeyDerivationPBKDF(CCPBKDFAlgorithm(kCCPBKDF2), // algorithm | |
passwordBytes, // password | |
password.count, // passwordLen | |
saltBytes, // salt | |
salt.count, // saltLen | |
CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA1), // prf | |
10000, // rounds | |
&derivedBytes, // derivedKey | |
length) // derivedKeyLen | |
} | |
} | |
guard status == 0 else { | |
throw Error.keyGeneration(status: Int(status)) | |
} | |
return Data(bytes: UnsafePointer<UInt8>(derivedBytes), count: length) | |
} | |
static func randomIv() -> Data { | |
return randomData(length: kCCBlockSizeAES128) | |
} | |
static func randomSalt() -> Data { | |
return randomData(length: 8) | |
} | |
static func randomData(length: Int) -> Data { | |
var data = Data(count: length) | |
let status = data.withUnsafeMutableBytes { mutableBytes in | |
SecRandomCopyBytes(kSecRandomDefault, length, mutableBytes) | |
} | |
assert(status == Int32(0)) | |
return data | |
} | |
} | |
@propertyWrapper | |
/// initalizer: @AESCCWrapper(saveKey: <#T##String#>, password: <#T##String#>, defaultValue: <#T##_#>, userDefaultsSuite: <#T##UserDefaults#>, aes: <#T##AES256?#>, errorHandler: <#T##((Error) -> ())?##((Error) -> ())?##(Error) -> ()#>) | |
/// if the value not saved yet, default value will be returned, and not SAVED | |
/// simplified initializer: @AESCCWrapper(saveKey: <#T##String#>, password: <#T##String#>, defaultValue: <#T##_#>) | |
struct AESCCWrapper<Value: Codable> { | |
var saveKey : String | |
var password: String | |
var defaultValue: Value | |
var userDefaultsSuite : UserDefaults = .standard | |
// FIXME: persistent salt instead of this | |
var aes: AES256? = nil | |
var errorHandler: ((Error)->())? | |
enum CustomError: Swift.Error { | |
case missingData, missingEncryptor, invalidInput | |
} | |
var wrappedValue : Value { | |
mutating get { | |
do { | |
guard let encrypted = self.userDefaultsSuite.object(forKey: saveKey) as? Data else { | |
throw CustomError.missingData | |
} | |
if self.aes == nil { | |
guard let aesData = self.userDefaultsSuite.object(forKey: "\(saveKey).AES") as? Data else { | |
throw CustomError.missingEncryptor | |
} | |
self.aes = try JSONDecoder().decode(AES256.self, from: aesData) | |
} | |
let decrypted = try self.aes!.decrypt(encrypted) | |
let value = try JSONDecoder.init().decode(Value.self, from: decrypted) | |
return value | |
} | |
catch { | |
self.errorHandler?(error) | |
return defaultValue | |
} | |
} | |
set { | |
do { | |
let salt = AES256.randomSalt() | |
let iv = AES256.randomIv() | |
let key = try AES256.createKey(password: password.data(using: .utf8)!, salt: salt) | |
if self.aes == nil { | |
self.aes = try AES256(key: key, iv: iv) | |
} | |
// FIXME: set nil to delete | |
if let encryptor = self.aes { | |
let encodedData = try JSONEncoder.init().encode(newValue) | |
let encrypted = try encryptor.encrypt(encodedData) | |
self.userDefaultsSuite.set(encrypted, forKey: saveKey) | |
let aesData = try JSONEncoder().encode(self.aes) | |
self.userDefaultsSuite.set(aesData, forKey: "\(saveKey).AES") | |
} | |
else { | |
throw CustomError.invalidInput | |
} | |
} | |
catch { | |
self.errorHandler?(error) | |
} | |
} | |
} | |
} | |
// TODO: Please remove this & switch to CryptoKit Chachapoly when our target deployment is iOS 13 and later |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment