Encrypting and decrypting

Revision as of 17:41, 22 September 2026 by Lchrisman (talk | contribs) (ER 22440: document the Crypto namespace -- EncryptionKey, Encrypt, Decrypt, RandomBytes, key->Export())


Release:

…  • 4.6 •  5.0 •  5.1 •  5.2 •  5.3 •  5.4 •  6.0 •  6.1 •  6.2 •  6.3 •  6.4 •  6.5 •   •  6.6 •  7.0 •  7.1 •  7.2

New to Analytica 7.2.

Analytica can encrypt and decrypt data using strong, standard, authenticated encryption. The four functions live in the Crypto namespace:

Variable Salt := Crypto::RandomBytes(16)
Variable MyKey := Crypto::EncryptionKey( password: 'correct horse battery staple', salt: Salt )
Variable Sealed := Crypto::Encrypt( 'launch codes', MyKey )
Variable Plain := Crypto::Decrypt( Sealed, MyKey ) → 'launch codes'

The Crypto namespace

These are specialist functions, so they are not in your model's namespace to begin with. A model that never encrypts anything is not troubled by them, and they cannot collide with your own identifiers. You reach them in either of two ways.

Qualify each call with Crypto::, as in the example above. Nothing is needed to make this work.

Or import the namespace once. Put Crypto on a line of its own in your model's NamespaceImports attribute, and then write the names bare:

Encrypt( 'launch codes', MyKey )

The rest of this page writes Crypto:: explicitly so that each example works whether or not you have imported the namespace.

What authenticated encryption means

All four algorithms here are AEAD ciphers -- Authenticated Encryption with Associated Data. Along with the encrypted data, a sealed value carries an authentication tag computed from the key. Crypto::Decrypt checks the tag before it returns anything, so a sealed value that has been altered -- even by a single bit, even by someone who cannot read it -- produces an error instead of altered data.

This is what you want, and it is not what simpler schemes give you. Encryption on its own hides data but does not stop an attacker from changing it in ways that change the decrypted result predictably. Nothing on this page lets you do unauthenticated encryption, deliberately.

The error from a failed decryption is deliberately the same whether the key was wrong, the «aad» was wrong, or the data was altered. Telling those cases apart would help an attacker work out a key one guess at a time.

Keys

A key is a value of its own type, made by Crypto::EncryptionKey. It carries the algorithm along with the key material, so a key and an algorithm can never disagree, and Crypto::Encrypt needs no «algorithm» parameter.

Make the key its own variable. Deriving a key from a password takes a noticeable fraction of a second by design (see Passwords and salts). When the key is a variable, Analytica computes it once and reuses it, so encrypting a whole column of a table costs one derivation, not one per cell. Writing the derivation inline inside Crypto::Encrypt would repeat it for every cell.

The key material itself cannot be read from any expression. Printing a key shows only what it is:

MyKey → «EncryptionKey AES-256-GCM #7a2f»

A key has four readable members:

  • MyKey->algorithm -- 'AES-256-GCM', 'AES-192-GCM', 'AES-128-GCM' or 'ChaCha20-Poly1305'.
  • MyKey->keyBits -- 128, 192 or 256.
  • MyKey->fingerprint -- four hexadecimal digits that identify the key without revealing it. Two keys are the same key if and only if their fingerprints match (to a very high probability). Useful for answering "did this deployment get the key I think it did?" without ever displaying a key.
  • MyKey->fromSecret -- True when the key material came from a Secret.

Passwords and salts

Crypto::EncryptionKey( password: p, salt: s ) stretches a password into a key using PBKDF2-HMAC-SHA-256 with «iterations» rounds, 600,000 by default. The iterations are the point: they make each guess at your password expensive for an attacker, which is the only defence a human-chosen password has.

The «salt» is not secret. Keep it in an ordinary variable next to the key, and save it with your model -- you need the same salt to derive the same key again. Its job is to make precomputed attack tables useless, which it does even though it is public. Make one with Crypto::RandomBytes(16) and then leave it alone; changing the salt changes the key, and data encrypted under the old key can no longer be read.

Keeping the key out of the model

If the key is written in the model, anyone who has the model has the key, and the encryption protects nothing. The usual arrangement is the other way round: the sealed data travels in the .ana file and the key does not.

Give «key» or «password» a Secret to do that. The secret's value reaches the key derivation without ever entering the model as a value:

Crypto::EncryptionKey( key: MyKeySecret )
Crypto::EncryptionKey( password: MyPasswordSecret, salt: Salt )

The Secret needs two things set before Analytica will allow this:

  • Sinks must list EncryptionKey.
  • Destination must be the single word local. Unlike a URL or a database connection string, key material never leaves the Analytica process, so there is no destination to pin -- but the setting is still required, so that allowing it is a deliberate act.

Consider also setting the secret's Caller to the module that is allowed to build the key, which restricts the use of the secret far more tightly than any destination could.

A key made from a Secret refuses ->Export(), and reads fromSecret = True. That is deliberate: a secret's value must never come back into the model, and exporting the key it produced would be exactly that.

Generating and saving a key

Crypto::EncryptionKey() with no arguments makes a fresh random key. To use the same key again later, export the material and store it somewhere outside the model -- a Secret, a file, a password manager:

Crypto::EncryptionKey()->Export() → a 32-byte binary value

To see it as text you can copy, format it:

f"{Crypto::EncryptionKey()->Export():b}" → base64'8Xk2wQ...'

Paste that base64'...' literal back into an expression to rebuild the same key:

Crypto::EncryptionKey( key: base64'8Xk2wQ...' )

A key cannot be stored in a variable the way other values can. Assigning one to a variable's definition is refused, because that would write the key material into the .ana file. Build the key where it is used instead.

Sealed values

Crypto::Encrypt returns binary data by default. The 'analytica' container adds 36 bytes to whatever you encrypted: a marker that says this is an Analytica sealed value, which algorithm made it, whether the plaintext was text or binary, the nonce, and the authentication tag.

Because the container records whether the plaintext was text or binary, Crypto::Decrypt hands back the same kind of value you encrypted, with nothing extra to say.

Every call produces a different result

Each call draws a fresh random nonce, so encrypting the same text twice gives two different sealed values. That is required -- reusing a nonce with the same key breaks the encryption completely -- but it has a practical consequence in a model: a sealed value computed by a definition changes every time the definition is re-evaluated.

So do not leave Crypto::Encrypt(...) in the definition of a variable whose result you intend to keep. Compute the sealed value once, in a button script, and assign it:

Sealed := Crypto::Encrypt( Plaintext, MyKey )

The assignment writes the sealed value into Sealed's definition as a base64'...' literal, which is saved with the model and decrypts unchanged when it is reopened.

Binding a sealed value to its context

The optional «aad» parameter -- additional authenticated data -- is authenticated but not encrypted. Whoever decrypts must supply the same «aad» or the decryption fails.

Use it to tie each sealed value to where it belongs. If you seal a column of salaries with no «aad», someone who cannot read them can still swap two rows and go undetected. Seal them with the employee as «aad» and the swap is caught:

Crypto::Encrypt( Salary, MyKey, aad: Employee )
Crypto::Decrypt( SealedSalary, MyKey, aad: Employee )

The «aad» is not secret and does not need to be hidden -- the reader has to know it anyway.

Encrypting a whole table

«data» and «aad» are atomic parameters, so both functions array abstract. One expression seals an entire indexed table, each cell under its own fresh nonce, all under the one key:

Variable SealedRows := Crypto::Encrypt( PlainRows, MyKey )
Variable PlainAgain := Crypto::Decrypt( SealedRows, MyKey )

This is where making the key its own variable pays off: the key is derived once and every cell reuses it.

Interoperating with other systems

To exchange sealed data with something outside Analytica, you need to agree on two things: how the bytes are packaged, and how they are written as text.

Layouts

There is no single universal convention for packaging a nonce, a ciphertext and an authentication tag, so «layout» lets you pick the one your counterpart uses.

«layout» The sealed value contains Used by
'analytica' (default) A self-describing container: marker, algorithm, text-or-binary flag, nonce, tag, ciphertext Analytica
'nonce,ct,tag' nonce, then ciphertext, then tag The common Go idiom, and most published examples
'ct,tag' ciphertext, then tag; the nonce is separate Python's cryptography package, libsodium
'nonce,tag,ct' nonce, then tag, then ciphertext
'none' the bare ciphertext; nonce and tag are separate Node.js crypto, .NET AesGcm

Whatever the layout, the nonce and the tag are also available as the second and third return values, so you can always get at them:

Local (sealed, nonce, tag) := Crypto::Encrypt( x, MyKey );

and supply them again when decrypting a layout that does not carry them:

Crypto::Decrypt( ct, MyKey, nonce: n, tag: t, layout: 'none', as: 'text' )

The raw layouts carry no record of whether the plaintext was text or binary, so «as» is required with them.

Text encodings

«format» writes the sealed value as text instead of binary data -- 'base64', 'base64url' (the URL-safe alphabet, no padding) or 'hex'. Crypto::Decrypt reads text in the same encodings.

Crypto::Encrypt( 'hello', MyKey, format: 'base64' )
Crypto::Decrypt( sealedText, MyKey, format: 'base64' )

A worked example

This Python produces a value Analytica reads, and reads a value Analytica produced. It uses the cryptography package.

from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import base64

key   = bytes(range(32))        # the same 32 bytes as base64'AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8='
nonce = bytes(range(12))

# Seal something for Analytica
ct = AESGCM(key).encrypt(nonce, 'from python'.encode('utf-8'), b'ctx7')
print(base64.b64encode(nonce + ct).decode())

# Open something Analytica sealed with layout:'ct,tag'
plain = AESGCM(key).decrypt(nonce, bytes.fromhex('66d9d9b2da0e0c4679f3a82524f5e0499271e16f30'), None)
print(plain.decode('utf-8'))    # -> hello

In Analytica:

Variable K := Crypto::EncryptionKey( key: base64'AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=' )
Crypto::Decrypt( base64'AAECAwQFBgcICQoLIXC5duWVu2/lLvmrU3Xrc1+3jRqJx0hC/3QKjDlK2w==', K, layout: 'nonce,ct,tag', aad: 'ctx7', as: 'text' ) → 'from python'
Crypto::Encrypt( 'hello', K, nonce: base64'AAAAAAAAAAAAAAAA', layout: 'ct,tag', format: 'hex' ) → '66d9d9b2da0e0c4679f3a82524f5e0499271e16f30'

The «nonce» parameter is used here only to make the output reproducible so it can be checked against a published test vector. Do not supply a nonce in real use. Reusing a nonce with the same key destroys the security of the encryption completely; omitting it is always the right thing.

The 'analytica' container

If you need to read Analytica's own container from another language, its bytes are:

Offset Length Field
0 4 The four characters ACRY
4 1 Format version, currently 1
5 1 Algorithm: 1 = AES-128-GCM, 2 = AES-192-GCM, 3 = AES-256-GCM, 4 = ChaCha20-Poly1305
6 1 Flags. Bit 0 set means the plaintext was text, encoded as UTF-8. Bit 1 set means an «aad» was supplied.
7 1 Nonce length, 12 for every algorithm above
8 12 Nonce
20 16 Authentication tag
36 rest Ciphertext

The salt and the iteration count are not in the container. They belong to the key, which you construct explicitly, so keep the salt in a variable of your own next to the key that uses it.

Function reference

Crypto::EncryptionKey( algorithm, key, keyFormat, password, salt, iterations, kdf )

Creates a key for Crypto::Encrypt and Crypto::Decrypt. Give it either «key» or «password», or neither for a fresh random key.

Parameters

  • «algorithm»: (optional, default 'AES-256-GCM') One of 'AES-256-GCM', 'AES-192-GCM', 'AES-128-GCM' or 'ChaCha20-Poly1305'. ChaCha20-Poly1305 requires Windows 10 version 1903 or later; on an older system it reports that it is unavailable rather than failing obscurely.
  • «key»: (optional) Raw key material, as binary data -- usually a base64'...' literal. It must be exactly the algorithm's key length: 32 bytes for AES-256-GCM and ChaCha20-Poly1305, 24 for AES-192-GCM, 16 for AES-128-GCM. A key of the wrong length is an error; Analytica will not pad or hash it to fit, because that would quietly weaken it. You may also pass a Secret here, in which case the secret's text is decoded according to «keyFormat». Plain text that is not a Secret is refused -- use «password» for a passphrase.
  • «keyFormat»: (optional, default 'base64') How to read the text a Secret holds: 'base64', 'base64url', 'hex' or 'utf8'. Meaningful only when «key» is given a Secret.
  • «password»: (optional) A passphrase to derive the key from. Requires «salt». May be given a Secret.
  • «iterations»: (optional, default 600000) PBKDF2 rounds. More is slower and safer. Use the same number every time, or you get a different key.
  • «kdf»: (optional, default 'PBKDF2-HMAC-SHA-256') The key-derivation function. This release supports only that one; the parameter exists so that adding others later does not change how you write the call.

A parameter that could not take effect is refused rather than quietly ignored -- «iterations» without «password», for example.

Members

->algorithm, ->keyBits, ->fingerprint and ->fromSecret, described under Keys. There is no member that returns the key material.

key->Export( )

Returns the raw key material of a key as binary data. This is the only way to get key material back out, and it is meant for saving a randomly generated key so the same key can be used again.

MyKey->Export() → a 32-byte binary value

A key derived from a Secret cannot be exported, and reports an error. A secret's value is never allowed back into the model.

Crypto::Encrypt( data, key, aad, nonce, format, layout )

Encrypts «data» under «key» and returns a sealed value that only the same key can open. Returns Null when «data» is Null.

Parameters

  • «data»: Text or binary data. A number is refused rather than converted, since 42 could reasonably mean either the text '42' or eight bytes -- use Text(x) if you mean its textual form.
  • «nonce»: (optional, named) Supplies the nonce instead of generating one. Reusing a nonce with the same key destroys the security of the encryption completely. Omit this unless you are reproducing a published test vector or another system's exact output.
  • «format»: (optional, named, default 'binary') 'binary', 'base64', 'base64url' or 'hex'.
  • «layout»: (optional, named, default 'analytica') See Layouts.

Return value

The sealed value, as binary data or as text according to «format». The nonce and the authentication tag are returned as the second and third return values:

Local (sealed, nonce, tag) := Crypto::Encrypt( x, MyKey );

It array abstracts over «data» and «aad», sealing each cell under its own nonce.

Crypto::Decrypt( data, key, aad, nonce, tag, format, layout, as )

Opens a value sealed by Crypto::Encrypt. Returns Null when «data» is Null.

Reports an error if the value cannot be decrypted -- wrong key, wrong «aad», or data that has been altered. The message deliberately does not say which, because that distinction would help an attacker. Use Try if your model should carry on regardless.

Parameters

  • «data»: The sealed value, as binary data, or as text in the encoding named by «format».
  • «key»: The same key the value was sealed with. If it is a key for a different algorithm, that is reported specifically, since it is a mistake you can act on rather than a failed decryption.
  • «aad»: (optional) Must equal the «aad» the value was sealed with.
  • «nonce», «tag»: (optional, named) Required for a «layout» that does not carry them.
  • «format»: (optional, named, default 'base64') How to read a textual «data»: 'base64', 'base64url' or 'hex'. Ignored when «data» is binary.
  • «layout»: (optional, named, default 'analytica') Must match the layout the value was sealed with. See Layouts.
  • «as»: (optional, named) 'text' or 'binaryData'. With the default 'analytica' layout it is optional, because the sealed value records which one was encrypted. For every other layout it is required. 'text' reports an error if the decrypted bytes are not valid UTF-8, which is usually a sign that you wanted 'binaryData'.

Crypto::RandomBytes( n )

Returns «n» cryptographically random bytes as binary data. Use it for a «salt», or for raw key material.

Variable Salt := Crypto::RandomBytes(16)
Variable NewKey := Crypto::EncryptionKey( key: Crypto::RandomBytes(32) )

«n» may not exceed 65536.

Each evaluation returns different bytes. Like Crypto::Encrypt, this means a definition that calls it does not hold still across re-evaluations -- assign the result once if you need it to stay the same.

Errors

Most mistakes are reported specifically, and can be caught with Try:

  • The «key» is the wrong length for the algorithm, or «key» was given plain text rather than key material or a Secret.
  • Both «key» and «password» were given, or «password» was given without a «salt», or a parameter such as «iterations» cannot take effect.
  • The algorithm is not one of the four, or is not available on this computer.
  • «data» is a number rather than text or binary data.
  • The sealed value is not an Analytica sealed value, or was sealed with a different algorithm, or the text is not valid for the stated «format».
  • A Secret was passed where a secret makes no sense, such as «aad» or «salt», neither of which is secret.
  • ->Export() was called on a key derived from a Secret.

The one deliberately vague error is a failed decryption, described under Crypto::Decrypt.

See Also

Comments
Loading comments...