apple/swift-crypto

CryptoKitError.authenticationFailure when decrypting an AES SealedBox

arielelkin opened this issue · 6 comments

Question Checklist

Question Description

I'm trying to decrypt existing data encrypted using AES GCM. I initialise a SealedBox with the encrypted data, but when I pass that to AES.GCM.open I get a .CryptoKitError.authenticationFailure

Below is some sample code below that reproduces the issue:

let secret = "my-256-bit-secret-my-secret-my-s"
let key = SymmetricKey(data: secret.data(using: .utf8)!)

let plain = "Say hello to my little friend!"
let nonce = try! AES.GCM.Nonce(data: Data(base64Encoded: "fv1nixTVoYpSvpdA")!)
let tag = Data(base64Encoded: "e1eIgoB4+lA/j3KDHhY4BQ==")!

// Encrypt
let sealedBox = try! AES.GCM.seal(plain.data(using: .utf8)!, using: key, nonce: nonce, authenticating: tag)

let ciphertext = sealedBox.ciphertext.base64EncodedString()
print("ciphertext: \(ciphertext)") // bWtTZkPAu7oXpQ3QpHvoTvc4NQgDTIycXHFJWvjk


let sealedBoxToDecrypt = try! AES.GCM.SealedBox(nonce: nonce,
                                                ciphertext: Data(base64Encoded: ciphertext)!,
                                                tag: tag)
let decrypted = try! AES.GCM.open(sealedBoxToDecrypt, using: key) // Thread 1: Fatal error: 'try!' expression unexpectedly raised an error: CryptoKit.CryptoKitError.authenticationFailure

Hey @arielelkin,

The problem with your code is that on the seal side, you're passing tag to authenticating. That means that tag is added as authenticated data to the GCM encryption operation.

You're not passing that same data on the open side of the API, resulting in inconsistent data being authenticated.

The other issue is that you should use the tag that was computed, not the hardcoded one you have.

Here's a fixed snipped of code:

let secret = "my-256-bit-secret-my-secret-my-s"
let key = SymmetricKey(data: secret.data(using: .utf8)!)

let plain = "Say hello to my little friend!"
let nonce = try! AES.GCM.Nonce(data: Data(base64Encoded: "fv1nixTVoYpSvpdA")!)
let tag = Data(base64Encoded: "e1eIgoB4+lA/j3KDHhY4BQ==")!

// Encrypt
let sealedBox = try! AES.GCM.seal(plain.data(using: .utf8)!, using: key, nonce: nonce)

let ciphertext = sealedBox.ciphertext.base64EncodedString()
print("ciphertext: \(ciphertext)") // bWtTZkPAu7oXpQ3QpHvoTvc4NQgDTIycXHFJWvjk

let sealedBoxToDecrypt = try! AES.GCM.SealedBox(nonce: nonce,
                                                ciphertext: Data(base64Encoded: ciphertext)!,
                                                tag: sealedBox.tag)
let decrypted = try! AES.GCM.open(sealedBoxToDecrypt, using: key) // Thread 1: Fatal error: 'try!' expression unexpectedly raised an error: CryptoKit.CryptoKitError.authenticationFailure

Hi @FredericJacobs

Thanks for your response. I understand the issue, I'm trying to understand how this works when you're passed a String or Data and not a SealedBox instance...

Should I therefore expect to be given a ciphertext as well as the tag that was computed during encryption?

If you're not passing a SealedBox, you can reconstruct it like you did in your example.

It can be initialized with the nonce, ciphertext and tag that are transmitted by the sender (who encrypted the payload).

See examples at:
https://developer.apple.com/documentation/cryptokit/performing_common_cryptographic_operations

swift-crypto seems to be behaving like intended. The Playground should provide further info on how to combine those.

Note that we do not provide security reviews of protocols and it's the caller's responsibility to correctly and safely distribute the relevant values.

Thanks!

Direct Encrypt and Decrypt in Single method
func testEncryptandDecrypt(){
let secret = "my-256-bit-secret-my-secret-my-s"
let key = SymmetricKey(data: secret.data(using: .utf8)!)
let plain = "Say hello to my little friend!"
let nonce = try! AES.GCM.Nonce(data: Data(base64Encoded: "fv1nixTVoYpSvpdA")!)
// Encrypt
let sealedBox = try! AES.GCM.seal(plain.data(using: .utf8)!, using: key, nonce: nonce)
let ciphertext = sealedBox.ciphertext.base64EncodedString()
print("ciphertext: (ciphertext)") // bWtTZkPAu7oXpQ3QpHvoTvc4NQgDTIycXHFJWvjk
let sealedBoxToDecrypt = try! AES.GCM.SealedBox(nonce: nonce,
ciphertext: Data(base64Encoded: ciphertext)!,
tag: sealedBox.tag)
let decrypted = try! AES.GCM.open(sealedBoxToDecrypt, using: key)

    print(String(decoding: decrypted, as: UTF8.self))
}

Encrypt and Decrypt in Single method by passing tag with Encrypt message
func testEncryptandDecryptFirstWay() {
let keyStr = "d5a423f64b607ea7c65b311d855dc48f36114b227bd0c7a3d403f6158a9e4412"
let key = SymmetricKey(data: Data(hex:keyStr))
let nonceData = Data(hex: "131348c0987c7eece60fc0bc")
let nonce: AES.GCM.Nonce = try! AES.GCM.Nonce(data: nonceData)
let plain = "This is first cypto graphy method"
let encyptedData = plain.asData.aesGCMEncypt(nonce: nonce, key: key)
var decyptedStr = ""
if let encyptedData = plain.asData.aesGCMEncypt(nonce: nonce, key: key) {
decyptedStr = encyptedData.aesGCMDecyrpt(nonce: nonce, key: key)
}
XCTAssertEqual(plain, decyptedStr)

extension String {
var asData: Data {
return self.data(using: .utf8) ?? Data()
}
}

extension Data {
func aesGCMEncypt(nonce: AES.GCM.Nonce, key: SymmetricKey) ->Data?{
// Encrypt
do {
let sealedBox = try AES.GCM.seal(self, using: key, nonce: nonce)
let ciphertext = sealedBox.ciphertext.base64EncodedString()
let tag = sealedBox.tag
let tagPlusCiphertext = tag + ciphertext.asData
return tagPlusCiphertext
}
catch let exceptioInfo {
debugPrint("Encyption exceptioInfo: (exceptioInfo)")
}
return nil
}

func aesGCMDecyrpt(nonce: AES.GCM.Nonce, key: SymmetricKey) -> String{
    let tag = self.subtract(0, 16)
    let ciphertextData = self.subtract(tag.count, self.count - tag.count)
    let ciphertext = ciphertextData.asString
    // Decypt
    var decodeStr: String = ""
    do {
        let sealedBoxToDecrypt = try AES.GCM.SealedBox(nonce: nonce,
                                                        ciphertext: Data(base64Encoded: ciphertext)!,
                                                        tag: tag)
        let decrypted = try AES.GCM.open(sealedBoxToDecrypt, using: key)

        decodeStr = String(decoding: decrypted, as: UTF8.self)
    } catch let exceptioInfo {
        debugPrint("Decyption exceptioInfo: \(exceptioInfo)")
    }

    return decodeStr
}
/* Substract data object by passing start bytes and needed number of bytes called length*/
public func subtract(_ start: Int, _ length: Int) ->Data {
    precondition(self.count >=  start + length,
                 "Invalid data range range. trying to find out of bound data")
    let allBytes = Array(Data(bytes: self.bytes, count: self.count))
    let partBytes = Array(allBytes[start..<start + length])
    let dataPart = Data(bytes: partBytes, count: partBytes.count)
    return dataPart
}
var asString: String {
    let str = String(decoding: self, as: UTF8.self)
    return str
}

}