Wire Format
The exact bytes Crypt and MagicEncrypter write, what they do not guarantee and how they differ from Laravel's Crypt.
On this page
Quick Reference#
| Part | Value |
|---|---|
| Cipher | AES-256 in CBC mode with PKCS7 padding |
| Key | 32 bytes: the UTF-8 bytes of a raw string (32 characters for ASCII), or the decoded bytes of a base64: key |
| IV | 16 random bytes per call from a secure random source |
| Plaintext | The string, UTF-8 encoded |
| Payload | base64(iv):base64(ciphertext), standard base64 with padding, one colon |
The format is fixed. It is the one Magic 0.0.41 and earlier wrote, so changing any part orphans every stored value.
Reading a Payload#
MagicEncrypter is the cipher behind both modes. Build one directly when you hold a key that is not app.key:
import 'package:magic_encryption/magic_encryption.dart';
void main() {
final MagicEncrypter encrypter = MagicEncrypter('0123456789abcdef0123456789abcdef');
final String payload = encrypter.encrypt('hello');
final List<String> parts = payload.split(':');
print(parts.length); // 2: the base64 IV, then the base64 ciphertext
print(encrypter.decrypt(payload)); // hello
}
MagicEncrypter(String key) throws an Exception unless the key is 32 bytes of UTF-8 (32 characters for ASCII). MagicEncrypter.fromAppKey(String) also accepts a base64: key and throws unless it decodes to 32 bytes. decrypt splits on :, base64-decodes both halves, decrypts and decodes UTF-8 with malformed bytes replaced by U+FFFD.
Not Authenticated#
There is no MAC and no AEAD tag. Two consequences follow:
- CBC is malleable. Someone who can modify a stored payload can flip bits in the IV and flip the same bits in the first plaintext block, predictably. Decryption still succeeds and returns a value the attacker partly chose.
- A decryption failure is observable. A caller that reports it back to whoever supplied the payload builds a padding oracle.
So Crypt keeps a value opaque at rest on the device and is not evidence that the value is unmodified. Verify anything whose integrity matters on the server that issued it, which can hold a secret this layer cannot.
Not Laravel#
Laravel's Crypt writes base64 of a JSON object holding the IV, the value and an HMAC, and refuses a payload whose MAC does not verify. This package writes base64(iv):base64(ciphertext) with no MAC. The two cannot read each other's output, so do not send a Magic payload to a Laravel Crypt::decrypt or the other way round. Exchange data with a server through the transport, not through this cipher.
Gotchas#
| Mistake | Fix |
|---|---|
Passing a Crypt payload to Laravel's decrypter |
It will not parse; the formats differ. |
| Relying on a failed decrypt to catch tampering | Most tampering still decrypts. Verify integrity on the server. |
| Pinning the IV in production | MagicEncrypter.encrypt has an iv: parameter for tests only; a reused IV leaks equality of plaintext prefixes. |
Building a MagicEncrypter from a key that is not 32 bytes of UTF-8 (32 characters for ASCII) |
It throws; use MagicEncrypter.fromAppKey for a base64: key. |