3

I need to have a python code and a swift code exchange encrypted message.

Here's what I tried:

  1. Fernet

After a review of the options, I thought that a symetric key algorithm could work well.

In python (as usual), it is straightforward to encrypt and decrypt:

Fernet(key).encrypt(b"mdg") # encrypt
Fernet(key).decrypt(encryptedMsg) # decrypt

In swift, it seemed initially straightforward with something along the lines of:

func encrypt(key: String, msg: String) throws -> String {
  let data = Data(base64URL: key)!
  let symetricKey = try! SymmetricKey(data: d)
  let msgUtf8 = msg.data(using: .utf8)!
  let sealBox = try! AES.GCM.seal(msgUtf8, using: symetricKey, nonce: nil)
  return sealBox.combined.base64EncodedString();
}

However, I have been unable to find the algorithm in swift matching python's Fernet.

  1. ChaCha

While searching for the problem, I landed on this amazing answer from Bram. Very unfortunately it only solves one side of my problem : encrypting messages in python and decoding them in swift. I also need the reverse process.

How to solve this?

DevShark
  • 8,558
  • 9
  • 32
  • 56

1 Answers1

1

To start, we first need a way to create secure random values to generate the IV and keys. You can also generate the keys using CryptoKit's SymmetricKey and extract the data from them, but for now, I'll use this function.

extension Data {
    static func secureRandom(ofSize size: Int) -> Data {
        var output = [UInt8](repeating: 0, count: size)
        _ = SecRandomCopyBytes(kSecRandomDefault, size, &output)
        return Data(output)
    }
}

We then require the possibility to compute the AES CBC ciphertext, which can be done using CommonCrypto.

func encrypt(plaintext: Data, key: Data, iv: Data) -> Data {
    var encryptor: CCCryptorRef?
    defer {
        CCCryptorRelease(encryptor)
    }

    var key = Array(key)
    var iv = Array(iv)
    var plaintext = Array(plaintext)

    CCCryptorCreate(CCOperation(kCCEncrypt), CCAlgorithm(kCCAlgorithmAES), CCOperation(kCCOptionPKCS7Padding), &key, key.count, &iv, &encryptor)

    var outputBytes = [UInt8](repeating: 0, count: CCCryptorGetOutputLength(encryptor, plaintext.count, false))
    CCCryptorUpdate(encryptor, &plaintext, plaintext.count, &outputBytes, outputBytes.count, nil)

    var movedBytes = 0
    var finalBytes = [UInt8](repeating: 0, count: CCCryptorGetOutputLength(encryptor, 0, true))
    CCCryptorFinal(encryptor, &finalBytes, finalBytes.count, &movedBytes)

    return Data(outputBytes + finalBytes[0 ..< movedBytes])
}

and the HMAC with the SHA-256 hash function. I recommend using CryptoKit's HMAC implementation here, but to keep things simple, I went with the CommonCrypto implementation.

func computeHMAC(_ data: Data, using key: Data) -> Data {
    var data = Array(data)
    var key = Array(key)
    var macOut = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
    CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA256), &key, key.count, &data, data.count, &macOut)
    return Data(macOut)
}

This brings all of this together into the following

let plaintext = Data("Hello world!".utf8)

let signingKey = Data.secureRandom(ofSize: kCCKeySizeAES128)
let cryptoKey = Data.secureRandom(ofSize: kCCKeySizeAES128)
let fernetKey = (signingKey + cryptoKey).base64EncodedString()

let version: [UInt8] = [0x80]
let timestamp: [UInt8] = {
    let timestamp = Int(Date().timeIntervalSince1970).bigEndian
    return withUnsafeBytes(of: timestamp, Array.init)
}()
let iv = Data.secureRandom(ofSize: kCCBlockSizeAES128)
let ciphertext = encrypt(plaintext: plaintext, key: cryptoKey, iv: iv)
let hmac = computeHMAC(version + timestamp + iv + ciphertext, using: signingKey)

let fernetToken = (version + timestamp + iv + ciphertext + hmac).base64EncodedString()

print("Fernet key: \(fernetKey)")
print("Fernet token: \(fernetToken)")

An example output can be

Fernet key: 7EwFlYNKTGfj+2fSgL3AUqtrRqRs4D1TWNK7t2XbGJQ=
Fernet token: gAAAAABivCLM0y0poDtGOohT1yK4XTDJppYPJdu4fuDTZ5tb9P9KP5ACgX8aJq4imsSdbzOCcvY3Tueo4FYbwyG+ZugozILL+Q==

We can use this in python using cryptography.io's implementation

from cryptography.fernet import Fernet

key = b'7EwFlYNKTGfj+2fSgL3AUqtrRqRs4D1TWNK7t2XbGJQ='
token = b'gAAAAABivCLM0y0poDtGOohT1yK4XTDJppYPJdu4fuDTZ5tb9P9KP5ACgX8aJq4imsSdbzOCcvY3Tueo4FYbwyG+ZugozILL+Q=='

Fernet(key).decrypt(token)
# b'Hello world!'
Bram
  • 2,718
  • 1
  • 22
  • 43
  • 1
    From doc of SecRandomCopyBytes: ”always test the returned status..”!! – Sajjon Jul 07 '22 at 13:00
  • @Sajjon yeah, as per demonstration, I did not do that. Even better would be to XOR several random sources, to get the best entropy. It all falls out of scope for this question. Besides, I’ve mentioned the usage of the SymmetricKey of CryptoKit, which does all the heavy lifting for us – Bram Jul 07 '22 at 21:34