c2sp.org/FLOE
This is the development version of this specification, rendered from the tip of the main branch.

Introduction §

Fast Lightweight Online Encryption (FLOE) is a cryptographic construction designed by Snowflake for the encryption of large files.

If any security issues are found with FLOE or the implementations, please contact us at security@snowflake.com.

The official Snowflake-owned copy of the specification is available at github.com/Snowflake-Labs/floe-specification. That repository also contains reference implementations, links to third-party implementations, and test vectors.

Motivation §

Snowflake, like many other companies, needs to works with very large files containing sensitive data. Encryption is one of many tools that we use to protect this data and fulfill our promises to our customers. Unfortunately, we determined that there are no good existing cryptographic constructions for the symmetric encryption of multi-gigabyte files.

All existing constructions fail one or more of our three primary requirements:

The first of these requirements is a basic requirement of any modern cryptographic construction. However, the need to validate the data upon decryption causes most constructions (such as AES-GCM) to need to hold the entire plaintext in memory before releasing it to the caller. In the case of a 2 gigabyte file, this means you are spending 2 gigabytes of memory just to hold data which you might normally be able to handle in a streaming manner. (For example, if you are downloading and decrypting a file from the network as part of writing it to local storage, you should not need to hold the whole file in memory at once. Instead, you should be able to stream it while only maintaining a small buffer of active data.) The "streaming" property described above is what is known technically as "online encryption."

Once we determined that there we no existing solutions, we came up with a list of additional requirements for FLOE. While none of these requirements would have prevented us from adopting an existing solution, if we need to build something new anyway, we want the result to be better in as many ways as possible.

Specification §

Fast Lightweight Online Encryption (FLOE) is a secure random access authenticated encryption (raAE) scheme as defined in Random-Access AEAD for Fast Lightweight Online Encryption. All secure (ra-ROR) raAE schemes are also nOAE2 secure as defined by HRRV15. FLOE is inspired heavily by the work in HRRV15 and others. FLOE can be thought of as a family of algorithms, each specified by a set a parameters. (This is similar to how HPKE is defined.)

This specification defines four public functions for the random access case (raAE): startEncryption, encryptSegment and their decryption equivalents. For usecases that do not require random access, we strongly recommend that instead of exposing encryptSegment and decryptSegment that you expose the online/sequential equivalents of them: encryptOnlineSegment, encryptLastSegment, and their decryption equivalents. These four methods (along with the two start functions) support the online/sequential use case and are harder to misuse. An implementation may choose not to expose those methods directly to callers but instead implement its own API on top of the "official" FLOE functions.

Terminology §

Both FLOE and its internal AEAD use data of similar types (keys, IVs, AADs, etc.). In all cases we explicitly specify which we're referring to.

All lengths are in bytes unless otherwise specified.

Parameters §

FLOE is parameterized by four values:

These parameters then define a large number of derived parameters.

Currently, only ENC_SEG_LEN can take different values. The other three parameters are fixed as follows:

Derived Parameters §

These parameters are all defined implicitly by selection of one of the main parameters listed above.

AEAD AEAD_ID AEAD_KEY_LEN AEAD_IV_LEN AEAD_TAG_LEN AEAD_ROTATION_MASK AEAD_MAX_SEGMENTS
AES-GCM-256 0 32 12 16 20 240
KDF KDF_ID KDF_KEY_LEN
HKDF-EXPAND-SHA-384 0 48

FLOE Ciphertext Layout §

A FLOE ciphertext consists of two parts: FLOE_HEADER and FLOE_BODY

The FLOE_HEADER consists of three parts:

  1. Parameter Information: 10 bytes
    PARAM_ENCODE(params)
  2. IV: FLOE_IV_LEN bytes
  3. Header tag: 32 bytes
    Output of FLOE_KDF(key, iv, aad, "HEADER_TAG:", 32)

The FLOE_BODY consists of zero or more internal segments and a single final segment. Each internal segment is exactly ENC_SEG_LEN bytes long. The final segment may be between AEAD_IV_LEN + AEAD_TAG_LEN + 4 and ENC_SEG_LEN (inclusive) bytes long.

A segment consists of four pieces:

  1. A final length value encoded with I2BE(*, 4)
    This value is max (0xFFFFFFFF) for all non-final segments and is the total encrypted segment length of the last segment. This means that it includes the lengths of the: length value, AEAD IV, AEAD ciphertext, and AEAD tag
  2. A random IV of AEAD_IV_LEN bytes
  3. A ciphertext encrypted with the AEAD. (The length is implicit and can be derived from context.)
  4. The tag of AEAD_TAG_LEN bytes

Key Generation §

FLOE keys MUST be of equal length to AEAD_KEY_LEN. They MUST meet the standard security requirements for symmetric keys of that length. (NIST SP 800-133, Section 6)

Implementation §

External Functions §

These are functions that are not specific to FLOE and are likely provided by external or standard libraries. While their behavior is defined, their implementation is out of scope for this specification.

Internal Functions §

These are FLOE-specific functions that may need to be implemented. None of these are exposed to callers. Depending on how you implement the code, these may be inlined, provided by the programming language, or otherwise refactored.

Semi-Public Functions (Random Access) §

FLOE can be defined in terms of four functions which support random access (as per the raAE definition). While this interface is a fully secure one (as per raAE) it does not protect developers against their own mistakes as much as the streaming/online interface. Thus, these methods should generally be internal implementation details. However, depending on the specific use-case, these APIs may be the correct level of abstraction to be made public. They are more challenging to use correctly because they no longer protect the developer from a number of mistakes:

In practice, this means that these API should likely not be exposed directly to developers but instead be used to construct higher-level (safe) APIs. For example, a developer of a client-side encryption library for cloud block storage, might choose to use FLOE. While they could simply use the online APIs above to stream the file to the cloud, using these random access APIs would permit them to spin up a number of threads to encrypt (and possibly upload) segments in parallel. Similarly, they could use these random access APIs to do random reads of the uploaded object.

startEncryption(key, aad) -> (State, Header)
  iv = RND(FLOE_IV_LEN)

  HeaderPrefix = PARAM_ENCODE(params) || iv
  HeaderTag = FLOE_KDF(key, iv, aad, "HEADER_TAG:", 32)
  MessageKey = FLOE_KDF(key, iv, aad, "MESSAGE_KEY:", KDF_KEY_LEN)
  Header = HeaderPrefix || HeaderTag

  State = {MessageKey, iv, aad}
  return (State, Header)
startDecryption(key, aad, header) -> State
  EncodedParams = PARAM_ENCODE(params)
  assert(len(header) == FLOE_IV_LEN + len(EncodedParams) + 32)

  (HeaderParams, iv, HeaderTag) = SPLIT(header, len(EncodedParams), 32)
  assert(HeaderParams == EncodedParams)

  ExpectedHeaderTag = FLOE_KDF(key, iv, aad, "HEADER_TAG:")
  if ctEq(ExpectedHeaderTag, HeaderTag) == FALSE: // Must be constant time
    throw("Invalid Header Tag")

  MessageKey = FLOE_KDF(key, iv, aad, "MESSAGE_KEY:", KDF_KEY_LEN)
  State = {MessageKey, iv, aad}
  return State
encryptSegment(State, plaintext, position, is_final) -> (State, EncryptedSegment)
  assert(len(plaintext) >= 0)
  if is_final:
    assert(len(plaintext) <= ENC_SEG_LEN - AEAD_IV_LEN - AEAD_TAG_LEN - 4)
    aad_tail = 0x01
  else:
    assert(len(plaintext) == ENC_SEG_LEN - AEAD_IV_LEN - AEAD_TAG_LEN - 4)
    aad_tail = 0x00

  aead_key = DERIVE_KEY(state.MessageKey, state.iv, state.aad, position) 
  aead_iv = RND(AEAD_IV_LEN)
  aead_aad = I2BE(position, 8) || aad_tail
  (aead_ciphertext, tag) = AEAD_ENC(aead_key, aead_iv, plaintext, aead_aad)

  if is_final:
    FinalSegmentLength = 4 + AEAD_IV_LEN + len(aead_ciphertext) + AEAD_TAG_LEN
    segment_header = I2BE(FinalSegmentLength, 4)
  else:
    segment_header = I2BE(0xFFFFFFFF, 4)

  EncryptedSegment = segment_header || aead_iv || aead_ciphertext || tag
  return (State, EncryptedSegment)
decryptSegment(State, EncryptedSegment, position, is_final) -> (State, Plaintext)
  if is_final:
    assert(len(EncryptedSegment) >= AEAD_IV_LEN + AEAD_TAG_LEN + 4)
    assert(len(EncryptedSegment) <= ENC_SEG_LEN)
    assert(BE2I(EncryptedSegment[:4]) == len(EncryptedSegment))
    aad_tail = 0x01
  else:
    assert(len(EncryptedSegment) == ENC_SEG_LEN)
    assert(BE2I(EncryptedSegment[:4]) == 0xFFFFFFFF)
    aad_tail = 0x00

  aead_key = DERIVE_KEY(state.MessageKey, state.iv, state.aad, position)
  (aead_iv, aead_ciphertext, tag) = SPLIT(EncryptedSegment[4:], AEAD_IV_LEN, AEAD_TAG_LEN)
  aead_aad = I2BE(position, 8) || aad_tail

  // Next line will throw if AEAD decryption fails
  Plaintext = AEAD_DEC(aead_key, aead_iv, aead_ciphertext, aead_aad, tag)

  return (State, Plaintext)

Public Streaming/Online Function §

These functions provide a safe interface to FLOE and are the recommended public API.

startOnlineEncryption(key, aad) -> (State, Header)
  (State, Header) = startEncryption(key, aad)
  State.Counter = 0
  State.Closed = False
  return (State, Header)
encryptOnlineSegment(State, plaintext) -> (State, EncryptedSegment)
  assert(State.Closed == False)
  assert(State.Counter != AEAD_MAX_SEGMENTS - 1)

  (State, EncryptedSegment) = encryptSegment(State, plaintext, State.Counter, False)

  State.Counter++
  return (State, EncryptedSegment)
encryptLastSegment(State, plaintext) -> EncryptedSegment
  assert(State.Closed == False)
  
  (State, EncryptedSegment) = encryptSegment(State, plaintext, State.Counter, True)

  State.Closed = True
  return EncryptedSegment
startOnlineDecryption(key, aad, header) -> State
  State = startDecryption(key, aad, header)
  State.Counter = 0
  State.Closed = False
  return State
decryptOnlineSegment(State, EncryptedSegment) -> (State, Plaintext)
  assert(State.Closed == False)
  assert(State.Counter != AEAD_MAX_SEGMENTS - 1)

  (State, Plaintext) = decryptSegment(State, EncryptedSegment, State.Counter, False)

  State.Counter++
  return (State, Plaintext)
decryptLastSegment(State, EncryptedSegment) -> Plaintext
  assert(State.Closed == False)

  (State, Plaintext) = decryptSegment(State, EncryptedSegment, State.Counter, True)

  State.Closed = True
  return Plaintext
Auxiliary Public Online Decryption Function §

This is a helper function which makes the FLOE API nicer to use but has no impact on its correctness or security properties.

decryptAnyOnlineSegment(State, EncryptedSegment) -> (State, Plaintext)
  if BE2I(EncryptedSegment[:4]) == 0xFFFFFFFF:
    return decryptOnlineSegment(State, EncryptedSegment)
  else:
    return decryptLastSegment(State, EncryptedSegment)