skip to Main Content

I have a requirement where a string will be encrypted from flutter web app using Dart. The encrypted string goes to server and the same will be consumed by iOS Swift.

Dart file


String encryptString(Uint8List key) {
   try {
    final encryptionKey = Key(key);
    final iv = IV.fromLength(16); // 16 bytes IV for AES-GCM
    final macValue = utf8.encode("");
    final encrypter = Encrypter(AES(encryptionKey, mode: AESMode.gcm));
    final encrypted = encrypter.encrypt(
      this,
      iv: iv,
      associatedData: macValue,
    );
    final concatenatedBytes = Uint8List.fromList([...iv.bytes, ...encrypted.bytes, ...macValue]);
    final base64Encoded = base64.encode(concatenatedBytes);
    return base64Encoded;
  } catch (e) {
    print('Error while encrypting: $e');
    return '';
  }
  }

However, the encrypted text I am not able to decrypt in Swift. I always get CryptoKit.CryptoKitError.authenticationFailure

Swift code

func decrypt(withKey key: SymmetricKey) throws -> String {
    guard let encryptedData = Data(base64Encoded: self) else { throw EncryptionError.invalidData }
    
    // Open the sealed box using the encryption key
    do {
      let sealedBox = try AES.GCM.SealedBox(combined: encryptedData)
      // Decrypt the data
      let decryptedData = try AES.GCM.open(sealedBox, using: key)
      // Convert decrypted data to string
      guard let decryptedString = String(data: decryptedData, encoding: .utf8) else {
        throw EncryptionError.decryptionFailed
      }
      return decryptedString
    } catch {
      print(error.localizedDescription)
      throw EncryptionError.invalidData
    }
  }

I tried the above snippets. Seems not to work, Please help 🙁

2

Answers


  1. Cryptographic schemes are all incompatible to each other. You’re using the default "SIC" scheme (AES in counter mode) on Dart and then you’re trying to decrypt using another scheme called GCM in Swift. That’s obviously not going to work, and the Dart library doesn’t seem to support authenticated ciphers such as GCM. Those are needed otherwise you’re prone to plaintext oracle attacks as well as having no message integrity or authentication (which, as you might imagine, is bad).

    You could try Fernet which is included in the "encrypt" library of Dart. It at least adds a HMAC tag for authentication, which basically makes it an authenticated cipher. A quick look showed this answer containing code in swift.

    Note that this is not a recommendation for the Dart or Swift code; I haven’t code reviewed it in any way. But without any crypto knowledge you could go worse than using a "sealed box" such as NaCL, Fernet or such. I did perform a protocol review for Fernet a long time ago, for what that’s worth.


    The Dart library raises a lot of red flags. It has great scores, but it doesn’t officially list any specifications and it doesn’t seem not notice that AES-SIC and AES-CTR are actually the same mode, which shows that the author and the community are probably rather inexperienced.

    Login or Signup to reply.
  2. Although GCM is not listed as a supported operation mode in the encrypt package documentation (Usage section), as you have already found out yourself, newer versions support GCM (see issue#259 and GCM-example), so this package can be used.
    Note that encrypt is only a high level wrapper for some PointyCastle functionalities, and that PointyCastle could be used directly as well.

    Apart from the incorrect operation mode (see other answer), both codes are still incompatible even after changing the Dart code to GCM mode. This is due to the nonce size of 16 bytes applied.
    For GCM, a 12 byte nonce is recommended, which should be adhered to for performance and compatibility reasons.
    The Swift library supports a 16 bytes nonce, but not in the combined representation, which is used in the current code and which may only be used for a 12 bytes nonce.
    Instead, the nonce, ciphertext and tag must be specified explicitly, i.e. they must be separated in the Swift code. Since the encrypt package uses the default tag length of 16 bytes and automatically appends the tag to the ciphertext, the data has the following structure: nonce (16 bytes) | ciphertext | tag (16 bytes), so that the parts can be separated using the known lengths of tag and nonce, e.g:

    let keyData = Data(base64Encoded: "MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDE=")
    let retrievedKey = SymmetricKey(data: keyData!)
    ...
    let data = Data(base64Encoded: "fzKLLUhOLMW7qJ6YhfFnv5TX6sD5gg2F6JxWkwhu07RpceWCu12EXzxgCqyB8VHXkXDHOOczaMXHLiDmlZqBJjgEb8IpmKJc/aLc")! // generated with Dart code
    let dataSize = data.count
    let noncebytes = data[0..<16]
    let ciphertext = data[16..<dataSize-16]
    let tag = data[dataSize-16..<dataSize]
    
    let nonce = try AES.GCM.Nonce(data: noncebytes)
    let sealedBox = try AES.GCM.SealedBox(nonce: nonce, ciphertext: ciphertext, tag:tag)
    let decryptedData = try AES.GCM.open(sealedBox, using: key)
    
    print(String(data: decryptedData, encoding: .utf8)!) // The quick brown fox jumps over the lazy dog
    

    Keep in mind, in the case of a 12 bytes nonce, the combined representation can be used. In this case, the data must have the following structure: nonce (12 bytes) | ciphertext | tag (16 bytes).

    Note that the name macValue in the Dart code is misleading. MAC is used as a synonym for the authentication tag. However, what you refer to as macValue is not the authentication tag but the additional authenticated data (AAD). This is data that does not need to be encrypted but should nevertheless be authenticated in order to ensure its integrity and authenticity.
    In contrast, the authentication tag (or MAC) is generated automatically during encryption (and, depending on the library, either appended to the ciphertext or returned separately). This tag is used during decryption to check the integrity and authenticity of the data.

    The current code uses an empty string for the AAD, i.e. effectively no AAD, so that consideration in the Swift code is not necessary. However, if AAD were to be used: In the case of the combined representation, the AAD must not be concatenated or must be removed before being passed to AES.GCM.SealedBox(). The AAD are explicitly specified in AES.GCM.open() (see authenticating).

    Incidentally, if the AAD are concatenated as in the current code (which is rather unusual), their length must be known to the decrypting side (at least if the ciphertext length is not known) so that separation is possible.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search