public final class SecureEnvelope

  1. Object
  2. SecureEnvelope

The one encrypted-blob format this package writes, and the only one it reads.

An envelope is self-describing: it carries the format version, the cipher suite, the key derivation parameters, the salt, the nonce, the identity and version of the key that sealed it, and the ciphertext with its tag. Everything except the ciphertext is authenticated but not encrypted, so a reader can decide what to do before it has a key, and an attacker who edits any of it fails the tag rather than steering the decryption.

Wire format, version 1

offset size  field
0      4     magic, the ASCII bytes CN1V
4      1     format version, 1
5      1     cipher suite, 1 = AES-256-GCM with a 12 byte nonce and a 128 bit tag
6      1     KDF id, 0 = none, 1 = PBKDF2-HMAC-SHA256
7      4     KDF iterations, 0 when the KDF id is 0
11     1     salt length, 0 to 64
12     n     salt
       1     nonce length, exactly 12 for suite 1
       n     nonce
       1     key id length, 0 to 64
       n     key id, UTF-8
       4     key version
       4     ciphertext length
       n     ciphertext, with the 16 byte GCM tag appended

All integers are big-endian. The associated data fed to AES-GCM is the whole header above, followed by the caller’s AssociatedData bytes – so the version, the suite, the iteration count and the key id are covered by the tag. That is what makes a downgrade attempt fail rather than succeed: an attacker who rewrites the iteration count to 1000 has changed the authenticated bytes, and the decrypt returns VaultError.AUTHENTICATION_FAILED.

What parsing rejects

A blob that is not this format, is truncated, declares a length that does not fit in the bytes present, declares a suite or version this build does not implement, or exceeds MAX_CIPHERTEXT is refused before any key is touched. No field is clamped into range on read, and there is no fallback to an earlier format: an envelope from the future is VaultError.UNSUPPORTED_FORMAT and not something to guess at.

Fields

public static final int VERSION_1 = 1The only format version this build writes.
public static final int SUITE_AES_256_GCM = 1AES-256-GCM, 12 byte nonce, 128 bit tag.
public static final int NONCE_LENGTH = 12GCM nonce length in bytes.
public static final int TAG_LENGTH = 16The GCM authentication tag length in bytes, appended to the ciphertext.
public static final int MAX_CIPHERTEXT = 67108864Ciphertext larger than this is refused on parse.

Methods

public static byte[] seal(byte[] key, String keyId, int keyVersion, AssociatedData aad, byte[] plaintext)Seals plaintext under a key the caller already has.
public static byte[] seal(byte[] key, String keyId, int keyVersion, byte[] associatedData, byte[] plaintext)The same, against associated data the caller has already serialized.
public static byte[] sealWithPassword(char[] password, KdfProfile profile, String keyId, int keyVersion, AssociatedData aad, byte[] plaintext)Seals plaintext under a key derived from a password.
public static SecureEnvelope parse(byte[] sealed)Parses an envelope without opening it.
public byte[] open(byte[] key, AssociatedData aad)Opens an envelope with a key the caller already has.
public byte[] open(byte[] key, byte[] associatedData)The same, against associated data the caller has already serialized.
public byte[] openWithPassword(char[] password, AssociatedData aad)Opens an envelope sealed with sealWithPassword.
public int getVersion()The format version, always VERSION_1 for anything this build parses.
public int getSuite()The cipher suite, always SUITE_AES_256_GCM for anything this build parses.
public KdfProfile getKdf()The derivation profile the envelope declares.
public String getKeyId()Which key sealed this, so a reader holding several can pick without trying each.
public int getKeyVersion()The key’s rotation counter at the time of sealing.
public byte[] getSalt()The salt, for a caller that derives the key itself rather than through openWithPassword.

Inherited methods

Field details

VERSION_1

public static final int VERSION_1 = 1
The only format version this build writes.

SUITE_AES_256_GCM

public static final int SUITE_AES_256_GCM = 1
AES-256-GCM, 12 byte nonce, 128 bit tag. The only suite version 1 defines.

NONCE_LENGTH

public static final int NONCE_LENGTH = 12
GCM nonce length in bytes. Twelve, which is the size AES-GCM is defined for; any other length goes through an extra derivation step that not every platform implements alike.

TAG_LENGTH

public static final int TAG_LENGTH = 16
The GCM authentication tag length in bytes, appended to the ciphertext.

MAX_CIPHERTEXT

public static final int MAX_CIPHERTEXT = 67108864
Ciphertext larger than this is refused on parse. Sixty-four megabytes is far past what a vault record should be, and the bound exists so a corrupt or hostile length field cannot make a reader allocate until it dies.

Method details

seal

public static byte[] seal(byte[] key, String keyId, int keyVersion, AssociatedData aad, byte[] plaintext)
Seals plaintext under a key the caller already has.

Parameters

key byte[]
32 bytes of AES-256 key material
keyId String
which key this is, so a reader with several can pick. At most 64 UTF-8 bytes
keyVersion int
the key’s rotation counter
aad AssociatedData
the binding this envelope may only be opened against
plaintext byte[]
the bytes to protect

Returns

the envelope bytes, safe to store or to send to a server that must not read them

seal

public static byte[] seal(byte[] key, String keyId, int keyVersion, byte[] associatedData, byte[] plaintext)

The same, against associated data the caller has already serialized.

AssociatedData is the spelling application code should use; this overload exists for the two callers that hold bytes rather than fields – the device-protection SPI, whose associated data is the port’s own, and an implementation of this format in another language checking itself against test vectors.

Parameters

key byte[]
32 bytes of AES-256 key material
keyId String
which key this is, at most 64 UTF-8 bytes
keyVersion int
the key’s rotation counter
associatedData byte[]
authenticated, not encrypted; may be null for none
plaintext byte[]
the bytes to protect

Returns

the envelope bytes

sealWithPassword

public static byte[] sealWithPassword(char[] password, KdfProfile profile, String keyId, int keyVersion, AssociatedData aad, byte[] plaintext)

Seals plaintext under a key derived from a password.

A fresh salt and nonce are generated for every call. Reusing either is the classic way to destroy AES-GCM, and there is no overload that lets a caller supply them.

Parameters

password char[]
the password, cleared by the caller afterwards
profile KdfProfile
the derivation profile, normally KdfProfile.current()
keyId String
which key this is
keyVersion int
the key’s rotation counter
aad AssociatedData
the binding this envelope may only be opened against
plaintext byte[]
the bytes to protect

Returns

the envelope bytes

parse

public static SecureEnvelope parse(byte[] sealed)

Parses an envelope without opening it.

Useful before a key is available: the key id and version say which key is needed, and getKdf() says how long deriving one will take, which is worth showing a user before a six hundred thousand iteration derivation begins.

Parameters

sealed byte[]
the envelope bytes

Returns

the parsed envelope

Throws

VaultException
VaultError.CORRUPT for a malformed blob, VaultError.UNSUPPORTED_FORMAT for a version, suite or KDF this build does not implement

open

public byte[] open(byte[] key, AssociatedData aad)
Opens an envelope with a key the caller already has.

Parameters

key byte[]
32 bytes of AES-256 key material
aad AssociatedData
the binding the envelope was sealed against. A different binding fails the tag, which is the point of it

Returns

the plaintext

Throws

VaultException
VaultError.AUTHENTICATION_FAILED when the tag does not verify – wrong key, altered ciphertext, or the wrong binding. No plaintext is returned in that case, not even a partial one

open

public byte[] open(byte[] key, byte[] associatedData)
The same, against associated data the caller has already serialized. See [#seal(byte[], String, int, byte[], byte[])] for why this overload exists.

Parameters

key byte[]
32 bytes of AES-256 key material
associatedData byte[]
the exact bytes the envelope was sealed against

Returns

the plaintext

openWithPassword

public byte[] openWithPassword(char[] password, AssociatedData aad)
Opens an envelope sealed with sealWithPassword.

Parameters

password char[]
the password, cleared by the caller afterwards
aad AssociatedData
the binding the envelope was sealed against

Returns

the plaintext

getVersion

public int getVersion()
The format version, always VERSION_1 for anything this build parses.

getSuite

public int getSuite()
The cipher suite, always SUITE_AES_256_GCM for anything this build parses.

getKdf

public KdfProfile getKdf()
The derivation profile the envelope declares. KdfProfile.needsUpgrade() on the result says whether it is worth rewrapping.

getKeyId

public String getKeyId()
Which key sealed this, so a reader holding several can pick without trying each.

getKeyVersion

public int getKeyVersion()
The key’s rotation counter at the time of sealing.

getSalt

public byte[] getSalt()
The salt, for a caller that derives the key itself rather than through openWithPassword.