Encrypting and Decrypting using Apple CryptoKit with user specified password

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.

Comments 6

  • 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.

  • 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.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.