YOUSEFUL / DELPHI

Atlas Crypto: FAQ, examples and tutorial

Learning center · Component reference

Atlas Crypto gives Delphi programs hashing, message authentication, password and key derivation, authenticated encryption of values, streams and files, and public-key signatures and key agreement, through a small API that is hard to misuse. It implements no algorithm itself: every hash, cipher, signature and random number comes from Windows CNG (bcrypt.dll), which the operating system maintains. Add Atlas.Crypto for byte helpers, random numbers, SHA-2 hashes, HMAC, PBKDF2, HKDF and raw AES-GCM; Atlas.CryptoBox for sealed boxes (TAtlasSecretBox) and chunked stream and file encryption (TAtlasStreamCrypt), the layer most applications should use; Atlas.CryptoKeys for ECDSA and ECDH on P-256 and RSA with OAEP and PSS; and Atlas.CryptoComponents for the two components on the Atlas Crypto palette page, TAtlasHashCalculator and TAtlasFileProtector. It depends on no other Atlas product. The runtime package requires only the RTL, so it works in services and console programs, on Windows 10 or later. The code has not yet been reviewed by a cryptographer; the FAQ says what has and has not been checked.

FAQ

What does Atlas Crypto implement itself? No cryptographic algorithm. SHA-2, HMAC, PBKDF2, AES-GCM, ECDSA, ECDH, RSA and the random generator are all Windows CNG calls. Atlas Crypto supplies the CNG declarations (Atlas.Crypto.Cng, written from the public SDK documentation because Delphi ships none), a validating layer over each primitive, three container formats that put the primitives together safely, and tests that check the wiring against published test vectors. The library's own code is limited to plumbing around CNG: argument checks, the containers, a constant-time comparison, rejection sampling for unbiased random ranges, and HKDF, written as a few HMAC calls following RFC 5869 and checked against that RFC's test cases.

Which class should I use to encrypt data? TAtlasSecretBox for values that fit in memory, such as bytes, text or a database field, sealed with a random key from NewKey or with a password. TAtlasStreamCrypt for streams and files of any size, processed in 64 KB authenticated chunks. TAtlasFileProtector when you want a component on a form: it wraps the password file and text functions and adds an Iterations property and an OnProgress event. TAtlasAesGcm is the raw primitive. It takes a nonce from the caller because test vectors need one, and using a nonce twice with the same key breaks GCM completely, so use it only to match an existing format.

What happens when something does not open? Opening either returns exactly what was sealed or raises EAtlasCryptoError; there is no way to get unauthenticated data out. A wrong key, a wrong password, altered, truncated or extended data, a different context value and data that is not a box at all all raise the same message, Decryption failed: wrong key or password, or the data was altered, so someone probing a service learns nothing from the wording. Setup mistakes, such as a key that is not 32 bytes or an empty password when sealing, raise their own specific messages. One case needs care: TAtlasStreamCrypt.Decrypt writes each chunk to the destination stream as soon as that chunk is authenticated, so a stream that fails part-way leaves genuine but incomplete output behind. DecryptFile avoids this by writing to <name>.part and deleting it on failure.

How are passwords protected, and why is sealing with one slow? A password is stretched with PBKDF2-HMAC-SHA256, 600,000 rounds by default (TAtlasKdf.DefaultPasswordIterations), with a fresh 16-byte random salt for every box. The slowness is the protection: each guess costs an attacker the same work. On the machine used to write this page one seal or open at the default cost took about 0.2 seconds. The count is stored in the box header and authenticated, so opening needs no setting, and a forged header asking for more than 20,000,000 rounds (TAtlasSecretBox.MaxIterations) is refused before any work is done. TAtlasFileProtector will not accept fewer than 10,000 rounds. There is no Argon2 or scrypt, because CNG offers neither.

Can another program read what Atlas Crypto seals? Not without code written for it. ASB1 (key box), ASP1 (password box) and ASF1 (stream) are this library's own containers, and their byte layout is described at the top of Atlas.CryptoBox.pas. The primitives inside are standard (AES-256-GCM, PBKDF2-HMAC-SHA256), so another implementation can be written from that description. No interoperability test against another cryptography library has been done yet.

How do I store and exchange keys? Keys are CNG key blobs held in TBytes: TAtlasKeyPair.PublicKey and PrivateKey. A private blob is as secret as a password, so keep it in protected storage (for example sealed with TAtlasSecretBox) and call Wipe when you are done with it. To exchange an ECDSA public key with another system, use TAtlasEcdsa.PublicKeyToXY and PublicKeyFromXY, which work with the two 32-byte curve coordinates; a point that is not on the curve is rejected. ECDSA signatures are the raw 64-byte r then s form (IEEE P1363), not DER. PEM and DER import and export (SPKI, PKCS#8) and X.509 certificates are not implemented.

What is not covered? Not implemented yet: PEM/DER key files and X.509 certificates; DER-encoded ECDSA signatures; ChaCha20-Poly1305, Ed25519 and X25519, the P-384 and P-521 curves, SHA-3, Argon2 and scrypt; keys held in the Windows key store or a TPM (NCrypt) and DPAPI; JWT/JOSE, PKCS#7/CMS, PGP and TLS helpers. Left out on purpose: ECB and unauthenticated CBC, PKCS#1 v1.5 encryption, raw RSA, and RSA keys shorter than 2048 bits. The file functions take a password only; to encrypt a file with a key, call TAtlasStreamCrypt.Encrypt on two file streams (see the examples).

Which platforms does it run on? Windows 10 or later, as Win32 and Win64 programs; both are built and tested. There are no macOS, Linux, Android or iOS back ends and no FireMonkey demo. The public API contains no Windows types, so a new back end would change only Atlas.Crypto.Cng and the code that calls it. FIPS mode, older Windows versions and ARM64 have not been tested.

Is it thread safe? Nothing in Atlas Crypto keeps global state, so the class functions (TAtlasHash.Compute, TAtlasSecretBox.Seal, TAtlasEcdsa.Sign and the rest) can be called from several threads at once. A TAtlasHash object is one running computation, and the components hold their settings in properties, so give each thread its own object.

Has the code been reviewed? Not by a cryptographer, and it should be before it protects anything valuable. The tests check the wiring against published known answers (FIPS 180 hashes, RFC 4231 HMAC, RFC 6070 and other PBKDF2 vectors, RFC 5869 HKDF, GCM specification cases 1, 2 and 4, and the RFC 6979 A.2.5 ECDSA signature), and check behaviour such as the detection of a flip in every single byte of a box. The AES-GCM and ECDSA expected values were not recomputed with a second implementation, RSA and ECDH are covered by round trips only, and constant-time behaviour relies on CNG and on the library's own comparison rather than on measurement. Memory hygiene is best effort: byte copies of keys and passwords are wiped, but Delphi strings holding passwords are not.

Why is the password a method parameter and not a property? A published property is written into the form file, and from there into source control and the executable. TAtlasFileProtector therefore takes the password in each call, and TAtlasHashCalculator has no secret at all. Keep passwords in memory for as short a time as you can.

Are SHA-1 and MD5 safe? Not for security. Both are broken, and they are offered (hkSHA1, hkMD5) only for checksums that are published with them and for talking to old systems. Signatures always hash with SHA-256, and the password functions of TAtlasSecretBox, TAtlasStreamCrypt and TAtlasFileProtector always use PBKDF2-HMAC-SHA256, whatever hash you pick elsewhere.

Examples

Hash text with SHA-256 and with the legacy SHA-1. Text is hashed as its UTF-8 bytes.

pascal
uses System.SysUtils, Atlas.Crypto;

procedure ShowDigests;
begin
  WriteLn(TAtlasBytes.ToHex(TAtlasHash.OfString(hkSHA256, 'abc')));
  WriteLn(TAtlasBytes.ToHex(TAtlasHash.OfString(hkSHA1, 'abc')));
end;

Prints the FIPS 180 values, ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad and a9993e364706816aba3e25717850c26c9cd0d89d.

Hash data that arrives in pieces. Update takes bytes or a buffer, and UpdateFromStream reads a stream from its current position to its end.

pascal
uses System.SysUtils, System.Classes, Atlas.Crypto;

procedure HashInPieces;
var
  Hash: TAtlasHash;
  Rest: TBytesStream;
begin
  Rest := TBytesStream.Create(TAtlasBytes.OfString('bc'));
  Hash := TAtlasHash.Create(hkSHA256);
  try
    Hash.Update(TAtlasBytes.OfString('a'));
    Hash.UpdateFromStream(Rest);
    WriteLn(TAtlasBytes.ToHex(Hash.Finish));
  finally
    Hash.Free;
    Rest.Free;
  end;
end;

Prints the same ba7816bf...f20015ad as hashing abc in one call. After Finish the object cannot be used again.

Authenticate a message with a shared key (HMAC), and check a received tag in constant time.

pascal
uses System.SysUtils, Atlas.Crypto;

procedure CheckMessageTag;
var
  Tag: TBytes;
begin
  Tag := TAtlasHash.Hmac(hkSHA256, TAtlasBytes.OfString('Jefe'), TAtlasBytes.OfString('what do ya want for nothing?'));
  WriteLn(TAtlasBytes.ToHex(Tag));
  WriteLn(TAtlasBytes.ConstantTimeEquals(Tag,
    TAtlasBytes.FromHex('5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843')));
end;

Prints the RFC 4231 test case 2 value 5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843, then TRUE.

Stretch a password into a key with PBKDF2. Real code uses 16 random salt bytes per password (TAtlasRandom.Bytes(16)) and TAtlasKdf.DefaultPasswordIterations; the fixed values here are a published test vector.

pascal
uses System.SysUtils, Atlas.Crypto;

procedure DerivePasswordKey;
var
  Key: TBytes;
begin
  Key := TAtlasKdf.Pbkdf2(hkSHA256, TAtlasBytes.OfString('password'), TAtlasBytes.OfString('salt'), 4096, 32);
  WriteLn(TAtlasBytes.ToHex(Key));
  TAtlasBytes.Wipe(Key);
end;

Prints c5e478d59288c841aa530db6845c4c8d962893a001ce4e11a4963873aa98134a.

Derive separate keys for separate purposes from one strong secret with HKDF. Different Info labels give unrelated keys.

pascal
uses System.SysUtils, Atlas.Crypto;

procedure SplitMasterKey;
var
  Master, EncryptionKey, SigningKey: TBytes;
begin
  Master := TAtlasRandom.Bytes(32);
  EncryptionKey := TAtlasKdf.Hkdf(hkSHA256, Master, nil, TAtlasBytes.OfString('encrypt v1'), 32);
  SigningKey := TAtlasKdf.Hkdf(hkSHA256, Master, nil, TAtlasBytes.OfString('sign v1'), 32);
  WriteLn(TAtlasBytes.ConstantTimeEquals(EncryptionKey, SigningKey));
  TAtlasBytes.Wipe(Master);
end;

Prints FALSE.

Make a session token and roll a die, both from the operating system's cryptographic generator.

pascal
uses System.SysUtils, Atlas.Crypto;

procedure MakeTokens;
begin
  WriteLn(TAtlasRandom.Token);
  WriteLn(TAtlasRandom.Below(6) + 1);
end;

Prints 43 URL-safe characters (32 random bytes), such as BvArsocl_2qy_UZjodFORgUb2OkAk6ZP2eq0cdyukYg, then a number from 1 to 6. Below avoids the bias that mod would introduce.

Seal a database field with a key, and tie the box to its record with a context value. The context is authenticated but not stored, so opening needs the same value.

pascal
uses System.SysUtils, Atlas.Crypto, Atlas.CryptoBox;

procedure SealAField;
var
  Key, Box: TBytes;
begin
  Key := TAtlasSecretBox.NewKey;
  Box := TAtlasSecretBox.Seal(Key, TAtlasBytes.OfString('account 0042-7719'), TAtlasBytes.OfString('customer:42'));
  WriteLn(Length(Box));
  WriteLn(TEncoding.UTF8.GetString(TAtlasSecretBox.Open(Key, Box, TAtlasBytes.OfString('customer:42'))));
  TAtlasBytes.Wipe(Key);
end;

Prints 49 (17 bytes of data plus 32 of overhead) and account 0042-7719. Opening with customer:43, or with no context, raises EAtlasCryptoError.

Seal text with a password and handle a failure. Passwords are case sensitive.

pascal
uses System.SysUtils, Atlas.Crypto, Atlas.CryptoBox;

procedure PasswordNote;
var
  Sealed: string;
begin
  Sealed := TAtlasSecretBox.SealText('correct horse', 'my note');
  try
    WriteLn(TAtlasSecretBox.OpenText('Correct horse', Sealed));
  except
    on E: EAtlasCryptoError do WriteLn(E.Message);
  end;
end;

Prints Decryption failed: wrong key or password, or the data was altered. Each call runs 600,000 PBKDF2 rounds, so this takes a moment.

Encrypt a file with a key rather than a password. The file functions take a password only, so use the stream functions on file streams. Unlike EncryptFile, this writes the target directly; write to a temporary name and rename it if a failure must not leave a partial file.

pascal
uses System.SysUtils, System.Classes, System.IOUtils, Atlas.Crypto, Atlas.CryptoBox;

procedure EncryptFileWithKey;
var
  Key: TBytes;
  Folder, PlainName, SealedName: string;
  Source, Dest: TFileStream;
  Opened: TBytesStream;
begin
  Folder := TPath.Combine(TPath.GetTempPath, 'atlas-crypto-key-file');
  TDirectory.CreateDirectory(Folder);
  PlainName := TPath.Combine(Folder, 'plan.txt');
  SealedName := PlainName + '.sealed';
  TFile.WriteAllBytes(PlainName, TAtlasBytes.OfString('Phase one: tests.'));
  Key := TAtlasSecretBox.NewKey;
  try
    Source := TFileStream.Create(PlainName, fmOpenRead or fmShareDenyWrite);
    try
      Dest := TFileStream.Create(SealedName, fmCreate);
      try
        TAtlasStreamCrypt.Encrypt(Source, Dest, Key);
      finally
        Dest.Free;
      end;
    finally
      Source.Free;
    end;
    WriteLn(TFile.GetSize(SealedName), ' bytes sealed');
    Source := TFileStream.Create(SealedName, fmOpenRead or fmShareDenyWrite);
    Opened := TBytesStream.Create;
    try
      TAtlasStreamCrypt.Decrypt(Source, Opened, Key);
      WriteLn(TEncoding.UTF8.GetString(Opened.Bytes, 0, Opened.Size));
    finally
      Opened.Free;
      Source.Free;
    end;
  finally
    TAtlasBytes.Wipe(Key);
    TDirectory.Delete(Folder, True);
  end;
end;

Prints 49 bytes sealed (a 16-byte header, the 17 bytes of text and one 16-byte tag), then Phase one: tests..

Report progress and cancel. The callback runs once per chunk (64 KB by default); setting Cancel stops the work with an EAtlasCryptoError.

pascal
uses System.SysUtils, System.Classes, Atlas.Crypto, Atlas.CryptoBox;

procedure EncryptWithProgress;
var
  Source, Dest: TBytesStream;
begin
  Source := TBytesStream.Create(TAtlasRandom.Bytes(200000));
  Dest := TBytesStream.Create;
  try
    try
      TAtlasStreamCrypt.Encrypt(Source, Dest, TAtlasSecretBox.NewKey,
        procedure(Done, Total: Int64; var Cancel: Boolean)
        begin
          WriteLn(Done, ' of ', Total);
          Cancel := Done >= 100000;
        end);
    except
      on E: EAtlasCryptoError do WriteLn(E.Message);
    end;
  finally
    Dest.Free;
    Source.Free;
  end;
end;

Prints 65536 of 200000, 131072 of 200000 and Encryption was cancelled.

Sign data with ECDSA on P-256 and verify it. Verify returns False for a wrong signature, altered data or another key.

pascal
uses System.SysUtils, Atlas.Crypto, Atlas.CryptoKeys;

procedure SignARelease;
var
  Pair: TAtlasKeyPair;
  Signature: TBytes;
begin
  Pair := TAtlasEcdsa.GenerateKeyPair;
  try
    Signature := TAtlasEcdsa.Sign(Pair.PrivateKey, TAtlasBytes.OfString('release 4.2, build 118'));
    WriteLn(TAtlasEcdsa.Verify(Pair.PublicKey, TAtlasBytes.OfString('release 4.2, build 118'), Signature));
    WriteLn(TAtlasEcdsa.Verify(Pair.PublicKey, TAtlasBytes.OfString('release 4.3, build 118'), Signature));
  finally
    Pair.Wipe;
  end;
end;

Prints TRUE, then FALSE.

Send data to someone who has an RSA key pair: seal the data with a random content key, and wrap that key with the recipient's public key (RSA-OAEP only takes short messages).

pascal
uses System.SysUtils, Atlas.Crypto, Atlas.CryptoBox, Atlas.CryptoKeys;

procedure SendToRecipient;
var
  Recipient: TAtlasKeyPair;
  ContentKey, WrappedKey, Box: TBytes;
begin
  Recipient := TAtlasRsa.GenerateKeyPair;
  ContentKey := TAtlasSecretBox.NewKey;
  Box := TAtlasSecretBox.Seal(ContentKey, TAtlasBytes.OfString('quarterly figures'));
  WrappedKey := TAtlasRsa.Encrypt(Recipient.PublicKey, ContentKey);
  TAtlasBytes.Wipe(ContentKey);
  // The recipient unwraps the content key with the private key and opens the box.
  ContentKey := TAtlasRsa.Decrypt(Recipient.PrivateKey, WrappedKey);
  WriteLn(TEncoding.UTF8.GetString(TAtlasSecretBox.Open(ContentKey, Box)));
  WriteLn(Length(WrappedKey));
  TAtlasBytes.Wipe(ContentKey);
  Recipient.Wipe;
end;

Prints quarterly figures and 384: the default key has 3,072 bits, and the wrapped key is as long as the modulus. Generating an RSA key pair is slow, so a real recipient makes one once and publishes the public half.

Check a downloaded file against its published checksum with the TAtlasHashCalculator component, created in code here. The expected value may be in either case and may carry surrounding spaces.

pascal
uses System.SysUtils, System.IOUtils, Atlas.Crypto, Atlas.CryptoComponents;

procedure VerifyDownload;
var
  Calculator: TAtlasHashCalculator;
  FileName: string;
begin
  FileName := TPath.Combine(TPath.GetTempPath, 'atlas-crypto-download.txt');
  TFile.WriteAllBytes(FileName, TAtlasBytes.OfString('abc'));
  Calculator := TAtlasHashCalculator.Create(nil);
  try
    WriteLn(Calculator.FileMatches(FileName, ' BA7816BF8F01CFEA414140DE5DAE2223B00361A396177A9CB410FF61F20015AD '));
    Calculator.Algorithm := hkMD5;
    WriteLn(Calculator.HashFile(FileName));
  finally
    Calculator.Free;
    TFile.Delete(FileName);
  end;
end;

Prints TRUE, then the MD5 value 900150983cd24fb0d6963f7d28e17f72.

Tutorial

Sample project: SealAndOpen.

Goal: hash text against a published known answer, seal a note with a password and with a key, and see every kind of tampering refused.

  1. Create a console application. In the IDE choose File > New > Console Application and save the project as SealAndOpen. Add the Atlas Crypto src folder to Project > Options > Building > Delphi Compiler > Search path, or install the Atlas Crypto runtime package and build with it. Replace the generated program text with the header below. The uses clause names the three units the program needs: Atlas.Crypto for hashes, byte helpers and EAtlasCryptoError, Atlas.CryptoBox for TAtlasSecretBox, and Atlas.CryptoComponents for TAtlasHashCalculator. The project compiles, with nothing to run yet.

    pascal
    program SealAndOpen;
    
    // Atlas Crypto quick start: hash text against a published known answer, seal a
    // message with a password, open it again, and watch a wrong password, a changed
    // character and a truncated box all get refused. Finishes on its own with exit
    // code 0 when every check passes.
    
    {$APPTYPE CONSOLE}
    
    uses
      System.SysUtils,
      Atlas.Crypto,
      Atlas.CryptoBox,
      Atlas.CryptoComponents;
  2. Add the constants. Sha256OfAbc is the SHA-256 value of abc published in FIPS 180, which the program will reproduce. Rounds lowers the PBKDF2 cost so that the program answers at once; you will remove it when you adapt the program.

    pascal
    const
      // FIPS 180 known answer: SHA-256 of "abc".
      Sha256OfAbc = 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad';
      Password = 'correct horse battery staple';
      Note = 'Meet at the old mill at noon.';
      // Fewer PBKDF2 rounds than the default 600,000 so the program answers at once.
      // Keep the default in real code; the count travels inside the sealed data.
      Rounds = 50000;
  3. Add two helpers. Check prints a passed check and raises on a failed one, so the program verifies itself. Raises runs an action and reports whether it raised EAtlasCryptoError, the one exception class that every Atlas Crypto failure uses.

    pascal
    procedure Check(Condition: Boolean; const What: string);
    begin
      if not Condition then raise Exception.Create('Check failed: ' + What);
      WriteLn('  ok: ', What);
    end;
    
    // True when Action raises EAtlasCryptoError, the one exception class every
    // Atlas Crypto failure uses.
    function Raises(const Action: TProc): Boolean;
    begin
      Result := False;
      try
        Action();
      except
        on E: EAtlasCryptoError do
        begin
          WriteLn('  refused: ', E.Message);
          Result := True;
        end;
      end;
    end;
  4. Hash a known answer. TAtlasHashCalculator is a component, but it needs no form: create it with a nil owner and free it yourself. HashText returns lower-case hexadecimal of the UTF-8 bytes. Setting Algorithm to hkSHA512 switches the hash, and TAtlasHash.OfString gives the same result as the component, as bytes.

    pascal
    procedure HashKnownAnswer;
    var
      Calculator: TAtlasHashCalculator;
    begin
      WriteLn('1. Hashing');
      Calculator := TAtlasHashCalculator.Create(nil);
      try
        WriteLn('  SHA-256 of "abc": ', Calculator.HashText('abc'));
        Check(Calculator.HashText('abc') = Sha256OfAbc, 'matches the published FIPS 180 value');
        Calculator.Algorithm := hkSHA512;
        Check(Length(Calculator.HashText('abc')) = 128, 'SHA-512 gives 64 bytes, 128 hexadecimal digits');
      finally
        Calculator.Free;
      end;
      Check(TAtlasBytes.ToHex(TAtlasHash.OfString(hkSHA256, 'abc')) = Sha256OfAbc, 'TAtlasHash.OfString agrees');
    end;
  5. Seal with a password. SealText returns URL-safe base64 text that carries the salt, the iteration count, the nonce, the ciphertext and the tag. OpenText gives the note back only for the right password and unaltered text. Character 12 of the sealed text encodes part of the salt, which is authenticated like everything else, so changing it is caught.

    pascal
    procedure SealWithPassword;
    var
      Sealed, Damaged: string;
    begin
      WriteLn('2. Sealing with a password');
      Sealed := TAtlasSecretBox.SealText(Password, Note, Rounds);
      WriteLn('  sealed: ', Sealed);
      Check(TAtlasSecretBox.OpenText(Password, Sealed) = Note, 'the right password opens it');
      Check(TAtlasSecretBox.SealText(Password, Note, Rounds) <> Sealed, 'sealing again gives different text: fresh salt and nonce');
      Check(Raises(procedure begin TAtlasSecretBox.OpenText('wrong password', Sealed); end), 'a wrong password is refused');
      // Character 12 lies inside the header (the salt), which is authenticated.
      Damaged := Sealed;
      if Damaged[12] = 'A' then Damaged[12] := 'B' else Damaged[12] := 'A';
      Check(Raises(procedure begin TAtlasSecretBox.OpenText(Password, Damaged); end), 'one changed character is refused');
      Check(Raises(procedure begin TAtlasSecretBox.OpenText(Password, Copy(Sealed, 1, Length(Sealed) - 4)); end), 'a truncated box is refused');
    end;
  6. Seal with a key and a context. NewKey returns 32 random bytes. The context note:17 is authenticated with the box but not stored in it, so the box opens only where the same context is given. Wipe overwrites the key and empties the array.

    pascal
    procedure SealWithKey;
    var
      Key, Box, Plain: TBytes;
    begin
      WriteLn('3. Sealing with a key and a context');
      Key := TAtlasSecretBox.NewKey;
      Box := TAtlasSecretBox.Seal(Key, TAtlasBytes.OfString(Note), TAtlasBytes.OfString('note:17'));
      WriteLn('  ', Length(Box), ' bytes for a ', Length(Note), '-character note');
      Plain := TAtlasSecretBox.Open(Key, Box, TAtlasBytes.OfString('note:17'));
      Check(TEncoding.UTF8.GetString(Plain) = Note, 'the same key and context open it');
      Check(Raises(procedure begin TAtlasSecretBox.Open(Key, Box, TAtlasBytes.OfString('note:18')); end), 'another context is refused');
      TAtlasBytes.Wipe(Key);
      Check(Length(Key) = 0, 'the key was wiped');
    end;
  7. Add the main block and run the program. Any failed check, or any unexpected exception, is printed and sets exit code 1.

    pascal
    begin
      try
        HashKnownAnswer;
        SealWithPassword;
        SealWithKey;
        WriteLn('All checks passed.');
      except
        on E: Exception do
        begin
          WriteLn(E.ClassName, ': ', E.Message);
          ExitCode := 1;
        end;
      end;
    end.
  8. Check the output. The sealed text differs on every run; everything else is the same:

    Example
    1. Hashing
      SHA-256 of "abc": ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
      ok: matches the published FIPS 180 value
      ok: SHA-512 gives 64 bytes, 128 hexadecimal digits
      ok: TAtlasHash.OfString agrees
    2. Sealing with a password
      sealed: QVNQMQAAw1DcAK3CUexA_OX6gQI7J9gMVdPreRdyw-UkguWhsXnXji4sRNFeOXVPym6vJlGza5kje6TN8ZSVh2ON36ToQVzNbFcYLOo6njU2
      ok: the right password opens it
      ok: sealing again gives different text: fresh salt and nonce
      refused: Decryption failed: wrong key or password, or the data was altered
      ok: a wrong password is refused
      refused: Decryption failed: wrong key or password, or the data was altered
      ok: one changed character is refused
      refused: Decryption failed: wrong key or password, or the data was altered
      ok: a truncated box is refused
    3. Sealing with a key and a context
      61 bytes for a 29-character note
      ok: the same key and context open it
      refused: Decryption failed: wrong key or password, or the data was altered
      ok: another context is refused
      ok: the key was wiped
    All checks passed.

    Every refusal carries the same message. To adapt the program, remove the Rounds argument so that SealText uses the default 600,000 rounds, then seal your own settings or notes. Use a record id or a column name as the context of a key box, so that a box copied to another record will not open there.

Troubleshooting

Decryption failed: wrong key or password, or the data was altered. One message covers every way opening can fail, on purpose. Check the inputs one by one: the password (case sensitive, compared as UTF-8), the key, the context value, and whether the data changed on the way (a re-encoded or line-wrapped text column, a file transferred in text mode, a truncated field). OpenText ignores white space before and after the text but not inside it. A key box does not open with the password functions and a password box does not open with a key, and the same holds for key and password streams.

The key must be 32 bytes; 16 given. Use NewKey, or a password. TAtlasSecretBox and TAtlasStreamCrypt use AES-256 only. Make keys with TAtlasSecretBox.NewKey, or derive 32 bytes (TAtlasSecretBox.KeySize) with TAtlasKdf.Pbkdf2 or TAtlasKdf.Hkdf. Only the raw TAtlasAesGcm accepts 16- and 24-byte keys.

Sealing or opening with a password takes a noticeable moment. That is the default 600,000 PBKDF2 rounds at work, and it is what makes guessing expensive. The cost of opening is chosen by whoever sealed the data, because the count is stored inside it. For unit tests pass a lower count to SealWithPassword, SealText or EncryptWithPassword, or set TAtlasFileProtector.Iterations (10,000 at least). In a user interface, run the call on a background thread; the class functions keep no shared state.

Iterations must be between 10000 and 20000000. Raised when TAtlasFileProtector.Iterations is set outside that range; the old value is kept. The class functions accept counts from 1 to 20,000,000, for tests, but TAtlasFileProtector refuses costs too low to protect anything.

Importing the ECDSA_P256 key failed (Windows status C000000D). The blob is not a usable P-256 key of the expected kind: a private key where a public key belongs, a truncated or re-encoded blob, or a key from the other algorithm. ECDSA keys (TAtlasEcdsa) and ECDH keys (TAtlasEcdh) are not interchangeable. TAtlasEcdsa.Verify raises in this case instead of returning False, so a broken setup cannot pass for a bad signature.

Encrypting with RSA (is the message longer than the key allows?) failed (Windows status C000000D). RSA-OAEP with SHA-256 takes at most the modulus size minus 66 bytes: 190 bytes for a 2048-bit key, 318 for 3072 bits. Seal the data with TAtlasSecretBox and encrypt only its 32-byte key with RSA, as in the examples.

E2003 Undeclared identifier: 'hkSHA256' (or 'EAtlasCryptoError'). TAtlasHashKind and the exception class are declared in Atlas.Crypto. A unit that uses only Atlas.CryptoComponents or Atlas.CryptoKeys must add Atlas.Crypto to its uses clause as well.

A file method raises EFOpenError, EFCreateError or EOSError rather than EAtlasCryptoError. Missing, locked or read-only files are reported by the RTL's file streams and by the final rename, not by Atlas Crypto. Catch both kinds around file operations. Source and destination must be different files is an EAtlasCryptoError: the file functions never encrypt a file in place.

The text is not URL-safe base64. TAtlasBytes.FromBase64Url accepts only A-Z, a-z, 0-9, - and _, without = padding or line breaks. Text from a standard base64 encoder must be converted first. The text that SealText and EncryptText produce is already in this form.