Skip to content

Seal

Namespace Nfty.Core.Publish in assembly Nfty.Core.

public static class Seal

The cryptography behind a sealed export: authenticated encryption of a whole Set under a passphrase, plus a header that says what is inside without giving any of it away.

Remarks

What this buys, precisely. Without the passphrase the payload is indistinguishable from noise and cannot be altered undetectably. That part is arithmetic and it holds. Against the person you gave the passphrase to it buys a refusal in the product and nothing more — nfty will not write the assets back out, and this format is open source, so somebody determined re-implements the reader. Every surface that offers sealing says so, because a security claim a user misreads is worse than no claim at all.

The payload is framed, which opens four attacks a single-buffer AEAD does not have — reordering frames, truncating the payload, appending to it, and splicing a frame in from another file sealed under the same passphrase. Each is closed, and it is worth being exact about BY WHAT, because the obvious answer is wrong and mutation-probing the suite is what showed it:

  • Reorder — the nonce, which is a per-file random prefix followed by the frame index. Frame 1's ciphertext under frame 0's nonce fails its tag.
  • Truncate — the header MAC, which covers the frame count and the plaintext length, so the loop still expects every frame and runs out of bytes.
  • Append — the explicit check for a trailing byte after the last frame. This one has no second layer: every declared frame authenticates perfectly and the extra bytes are simply never read. It is the only guard here that is load-bearing alone.
  • Splice — three independent layers, any one of which suffices: the salt is per file, so two seals under one passphrase do not even share a KEY; the nonce prefix is per file; and the frame's associated data names the file's sealId. Removing all three is what it takes to make a grafted frame authenticate.

Nonces cannot repeat. Four random bytes fixed per file, then the frame index as a big-endian 64-bit integer. Reusing a nonce under one key is the single failure that breaks AES-GCM outright, and this construction makes it unrepresentable rather than merely unlikely.

PBKDF2 rather than Argon2, at 600,000 SHA-256 iterations — the OWASP figure for that pair. Argon2 is the better KDF and is not in the BCL; taking a cryptography dependency is a larger decision than this feature should make on its own. The iteration count is recorded per file, so raising it later costs nothing and orphans no existing seal.

Fields

FrameBytes

public const int FrameBytes = 1048576;

Plaintext bytes per encrypted frame.

Iterations

public const int Iterations = 600000;

PBKDF2 iterations this build writes. An older seal is read at whatever it records.

ManifestEntry

public const string ManifestEntry = "seal.json";

The plaintext header entry inside a sealed archive.

MinimumPassphraseLength

public const int MinimumPassphraseLength = 12;

The shortest passphrase this will seal with.

Remarks

A floor rather than advice, because the iteration count above is irrelevant against a passphrase that fits in a wordlist: sealing with "secret" produces a file that looks encrypted and is not, and nothing on screen would show the difference. Refusing is the honest response.

PayloadEntry

public const string PayloadEntry = "payload.bin";

The encrypted payload entry inside a sealed archive.

UnsupportedMessage

public const string UnsupportedMessage = "This platform provides no AES-GCM, so nfty cannot seal or open a sealed export here.";

Why sealing is unavailable, for a surface that wants to say so.

Properties

IsSupported

public static bool IsSupported { get; }

Whether this platform can seal at all.

Remarks

Public so a front-end can ASK before it offers the choice. AES-GCM is a platform capability rather than a language feature — .NET exposes it only where the underlying crypto library provides it — so on a host without it, sealing is not a thing that fails, it is a thing that is not there. A checkbox that accepts a passphrase, a confirmation and a note and then throws is a worse answer than one that is switched off with a reason next to it.

Every entry point here still checks it independently: a front-end reading this is a courtesy, not the enforcement.

Methods

ExtractTo(string, string, Stream)

public static SealManifest ExtractTo(string path, string passphrase, Stream plaintext);

Decrypts a sealed archive's payload into plaintext.

Parameters

  • path (string) - The sealed archive.
  • passphrase (string) - The passphrase it was sealed with.
  • plaintext (Stream) - Where the decrypted bytes go.

Returns. The header, verified against the passphrase.

Exceptions

  • SealedSetException - The passphrase is wrong, or the file was altered — two states nothing here can tell apart, reported as one.

IsUsablePassphrase(string?)

public static bool IsUsablePassphrase(string? passphrase);

Whether a passphrase is long enough to be worth deriving a key from.

Parameters

  • passphrase (string?) - The candidate.

Returns. True when it meets Seal.MinimumPassphraseLength.

Peek(string)

public static SealManifest Peek(string path);

Reads a sealed archive's header. No passphrase, because a recipient who has not found the passphrase yet still needs to be told what they are holding.

Parameters

  • path (string) - The sealed archive.

Returns. What the file says about itself in the clear.

Exceptions

Write(string, Stream, string, int, string?, SealPolicy, string)

public static void Write(string path, Stream plaintext, string collection, int count, string? note, SealPolicy policy, string passphrase);

Seals plaintext into a new archive at path.

Parameters

  • path (string) - Where to write it. An existing file there is replaced.
  • plaintext (Stream) - The bytes to encrypt — a packed Set. Must be seekable: the frame count and the plaintext length are authenticated by the FIRST frame, so both have to be known before a byte is written.
  • collection (string) - The collection's name, shown to a recipient with no key.
  • count (int) - How many assets are inside, shown to a recipient with no key.
  • note (string?) - What the author wants read first, or null.
  • policy (SealPolicy) - What nfty will let the recipient do.
  • passphrase (string) - The passphrase. Never stored, never written, never logged.

Exceptions

  • ArgumentException - The passphrase is too short, or the stream cannot seek.
  • SealedSetException - This platform provides no AES-GCM.