Last active
November 1, 2022 01:56
-
-
Save luannguyenkhoa/d0620df0de7296ce4e8e6c3d0a187fc5 to your computer and use it in GitHub Desktop.
A way to manage UserDefault in Swift by utilizing the combination of Enum and Protocol, combined with CryptoSwift to encrypt the sensitive info before storing.
This file contains 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 CryptoSwift | |
/* | |
Where controls all actions related to save and retrieve value from User Default | |
All strings value will be encrypted/decrypted before saving/retrieving from UD | |
Other types will be save/retrieve directly | |
*/ | |
public protocol UD { | |
associatedtype T | |
var rawValue: String {get} | |
} | |
public extension UD { | |
// MARK: - Getter and Setter | |
var value: T? { | |
let val = UserDefaults.standard.value(forKey: key) as? T | |
return decrypt(val: val) | |
} | |
func set(ud: UserDefaults? = UserDefaults.standard, _ val: T?) { | |
let value = encrypt(val) | |
ud?.set(value, forKey: key) | |
} | |
func instance(_ name: String) -> UserDefaults? { | |
return UserDefaults(suiteName: name) | |
} | |
func archive<E: Codable>(val: E) { | |
let archiver = NSKeyedArchiver() | |
do { | |
try archiver.encodeEncodable(val, forKey: NSKeyedArchiveRootObjectKey) | |
UserDefaults.standard.set(archiver.encodedData, forKey: key) | |
} catch let err { | |
print("Archive: \(err)") | |
} | |
} | |
func unarchive<E: Codable>() -> E? { | |
if let data = UserDefaults.standard.value(forKey: key) as? Data { | |
do { | |
let val = try NSKeyedUnarchiver(forReadingWith: data).decodeTopLevelDecodable(E.self, forKey: NSKeyedArchiveRootObjectKey) | |
return val.map({ $0 }) | |
} catch let err { | |
print("Unarchive: \(err)") | |
} | |
} | |
return nil | |
} | |
var bool: Bool { | |
return UserDefaults.standard.bool(forKey: key) | |
} | |
// MARK: - Private handlers | |
private var key: String { | |
return rawValue | |
} | |
private var secureKey: String { | |
return key.count > 16 ? String(key.prefix(16)) : key + String(repeating: "k", count: 16 - key.count) | |
} | |
// MARK: - Encrypt and Decrypt String using AES Method | |
private func encrypt(_ val: T?) -> T? { | |
/// Only encrypt the param that is in String type | |
guard let value = val as? String else { return val } | |
do { | |
/// Initialize AES with iv and securekey in 16 bytes | |
let iv = AES.randomIV(AES.blockSize) | |
let aes = try AES(key: secureKey.md5().bytes, blockMode: CBC(iv: iv)) | |
/// Start encrypting the inputting value | |
let encrypted = try aes.encrypt(value.bytes) | |
/// Shift iv key to the result to use for decrypt later | |
let encryptedK = Data(bytes: encrypted) | |
var encryptedKIV = Data(bytes: iv) | |
encryptedKIV.append(encryptedK) | |
/// Encode based64 data to end string | |
return encryptedKIV.base64EncodedString() as? T | |
} catch let err { | |
print(err) | |
return val | |
} | |
} | |
private func decrypt(val: T?) -> T? { | |
/// Ignore if the param is not a string | |
guard let value = val as? String else { return val } | |
do { | |
/// Ignore if its not a base64Encoded | |
guard let data = Data(base64Encoded: value) else { return val } | |
let count = [UInt8](data).count | |
/// Assumption based on encrypt method, the data size must be always greater than iv key size | |
guard count > AES.blockSize else { return val } | |
/// Get iv key part from the decoded data | |
let iv = Array([UInt8](data)[0 ..< AES.blockSize]) | |
/// Get target data | |
let bytes = Array([UInt8](data)[AES.blockSize ..< count]) | |
/// Decrypt data by AES method | |
let aes = try AES(key: secureKey.md5().bytes, blockMode: CBC(iv: iv)) | |
let decrypted = try aes.decrypt(bytes) | |
return String(bytes: decrypted, encoding: .utf8) as? T | |
} catch let err { | |
print(err) | |
return val | |
} | |
} | |
} | |
/// for dealling with UserDefault | |
public enum UDKey<T>: String, UD { | |
// MARK: Usecases | |
case example = "EXAMPLE" | |
/// Group cases into a nested Enum based on their sense | |
enum User: String, UD, CaseInterable { | |
case email = "USER_EMAIL" | |
case userId = "USER_ID" | |
case userName = "USER_NAME" | |
static func clean() { | |
User.allCases.forEach({ $0.set(nil) }) | |
} | |
} | |
enum Item: String, UD { | |
case state = "ITEM_STATE" | |
case read = "ITEM_READ" | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Attempts:
Retrieve value:
For first level Enum:
For nested Enum: