The examples provided in the documentation from Apple CryptoKit
show examples where you use the same key
bot for encryption and decryption.
What it doesn’t specify how this practically works. How do you make the key and how do you make such thing work in practice.
This article given an example how this can be done. It assumes that the CrytoKit library
is already installed. See https://github.com/apple/swift-crypto for instructions.
Step 1 – create the key
/// Create an ecnryption key from a given password
/// - Parameter password: The password that is used to generate the key
func keyFromPassword(_ password: String) -> SymmetricKey {
// Create a SHA256 hash from the provided password
let hash = SHA256.hash(data: password.data(using: .utf8)!)
// Convert the SHA256 to a string. This will be a 64 byte string
let hashString = hash.map { String(format: "%02hhx", $0) }.joined()
// Convert to 32 bytes
let subString = String(hashString.prefix(32))
// Convert the substring to data
let keyData = subString.data(using: .utf8)!
// Create the key use keyData as the seed
return SymmetricKey(data: keyData)
}
The above method will create an encryption/decryption key where the password is used as the seed. The key must have a length of 256 bits, i.e. 32 bytes.
This will then result in this initial code:
import Foundation
import Crypto
let myPassword = "my secret"
let key = keyFromPassword(myPassword)
Step 2 – Encrypt the data
For the encryption we create a method that can encrypt Codable
objects:
/// Encrypt the given object that must be Codable and
/// return the encrypted object as a base64 string
/// - Parameters:
/// - object: The object to encrypt
/// - key: The key to use for the encryption
func encryptCodableObject<T: Codable>(_ object: T, usingKey key: SymmetricKey) throws -> String {
// Convert to JSON in a Data record
let encoder = JSONEncoder()
let userData = try encoder.encode(object)
// Encrypt the userData
let encryptedData = try ChaChaPoly.seal(userData, using: key)
// Convert the encryptedData to a base64 string which is the
// format that it can be transported in
return encryptedData.combined.base64EncodedString()
}
This will then lead to the following incremental code:
import Foundation
import Crypto
let myPassword = "my secret"
let key = keyFromPassword(myPassword)
// A sample structure to encode
struct User: Codable {
let name: String
let password: String
}
// Create a user that will be encrypted
let user = User(name: "J.Doe", password: "Another Secret")
let base64EncodedString = try encryptCodableObject(user, usingKey: key)
// base64EncodedString will have value:
// fkcOR/H0nRWHemOPf3eUUxQGO2NR2nSC4vz0no1N5Dnd5Ia\
// qXiqtxeZeJb8hY0k+M8YhE+5uzZL85o7WUjJWq/8A0IDO+kNt
Step 3 – Decrypt the data
For the decryption we create a method that can decrypt the base64 string into
the target Codable
` object using the decryption key.
/// Decrypt a given string into a Codable object
/// - Parameters:
/// - type: The type of the resulting object
/// - string: The string to decrypt
/// - key: The key to use for the decryption
func decryptStringToCodableOject<T: Codable>(_ type: T.Type, from string: String,
usingKey key: SymmetricKey) throws -> T {
// Convert the base64 string into a Data object
let data = Data(base64Encoded: string)!
// Put the data in a sealed box
let box = try ChaChaPoly.SealedBox(combined: data)
// Extract the data from the sealedbox using the decryption key
let decryptedData = try ChaChaPoly.open(box, using: key)
// The decrypted block needed to be json decoded
let decoder = JSONDecoder()
let object = try decoder.decode(type, from: decryptedData)
// Return the new object
return object
}
which then leads to the final code:
import Foundation
import Crypto
let myPassword = "my secret"
let key = keyFromPassword(myPassword)
// A sample structure to encode
struct User: Codable {
let name: String
let password: String
}
// Create a user that will be encrypted
let user = User(name: "J.Doe", password: "Another Secret")
let base64EncodedString = try encryptCodableObject(user, usingKey: key)
// base64EncodedString will have value:
// fkcOR/H0nRWHemOPf3eUUxQGO2NR2nSC4vz0no1N5Dnd5Ia\
// qXiqtxeZeJb8hY0k+M8YhE+5uzZL85o7WUjJWq/8A0IDO+kNt
let newObject = try decryptStringToCodableOject(User.self, from: base64EncodedString, usingKey: key)
print(newObject.name) // J.Doe
print(newObject.password) // Another Secret
The intermediate base64 string can be stored in for example the keychain or send through a socket or email. When the receiving side knows the password they can construct the decryption key and decrypt the message.
Thank you! After days on the internet trying to find someone who actually did something in practice where a key can be shared simply this was really good to find. This is a very good example.
Thanks! Really helpful!
In the `keyFromPassword` function, why can’t you just create the key like this:
“`
let hash = SHA256.hash(data: password.data(using: .utf8)!)
return SymmetricKey(data: hash)
“`
I don’t understand the purpose of converting the hash to a string and then back into data. It doesn’t seem to add any security.
It has been a while since I created this method. If I remember correctly the Key must have an explicit length. The hash that is created is too long and needs to be shortened.
I could be mistaken, so why don’t you give it a try and let us know if that works.
The hash that is created from `SHA256.hash` is 32 bytes, or 256 bits. This is an appropriate length for the `SymmetricKey(data:)` initializer. There’s no need to resize the data. Even if you did need to resize the data, It still doesn’t make any sense to convert it to a string. I’ve tested my version and I can confirm that it works.
Thanks for the feedback.