1. ecfg(5)
  2. Version 0.3.1
  3. ecfg(5)

NAME

ecfg - JSON, YAML, or TOML file with asymmetric-key-encrypted values

SYNOPSIS

An ecfg file is syntactically a json, yaml, or toml file, but with a few minor semantic additions described below.

PUBLIC KEY

Each ecfg file must have a key at the top level named _public_key. This implies that the top-level structure must be a hashmap, not an array.

The _public_key key must have a string value, which is a hex-encoded 32-byte (totalling 64 ASCII bytes) public key as generated by ecfg-keygen(1).

By convention, _public_key should be the first key in the file.

ENCRYPTABLE VALUES

A value is considered encryptable if:

  1. It is a string literal (numbers, true, false, null all remain unencrypted);

  2. It is not an object key (ie. not immediately followed by a ":" in JSON, etc.);

  3. Its corresponding object key did not begin with an underscore ("_").

Take special note of point 3. This is the reason _public_key isn't encrypted, and can be used to construct metadata schemes. For example, in the excerpt below, only the values for rotation_password and secret will be encrypted.

"my_secret": {
  "_description": "API key for foocorp",
  "_rotation": "https://example.com/foocorp/apikey",
  "_rotation_username": "admin",
  "rotation_password": "password",
  "secret": "123123123123123123123"
}

Also note that this underscore "unencryptable" attribute is not heritable. For example, the password in this excerpt will be encrypted.

"_unencryptable": {
  "password": "encrypted anyway"
}

SECRET SCHEMA

When a value is encrypted, it will be replaced by a relatively long string of the form "EJ[V:P:N:M]". The fields are:

ENCRYPTION ALGORITHMS

ecfg values are encrypted using a Curve25519 x Salsa20 x Poly1305-AES public-key scheme. This normally implies use of NaCl or libsodium.

NaCl libraries generally take keys as a sequence of raw bytes, but they're embedded in ecfg files as hex-encoded strings, so we need a routine to convert them:

base16_to_raw(key : string) -> []byte =
  # convert e.g. "1234beef" into [0x12, 0x34, 0xBE, 0xEF] or whatever
  # the particular NaCl implementation/binding wants.

When we write the final encrypted message according to the SECRET SCHEMA section, we need to encode several sequences of raw bytes to base64, with newlines removed:

encode(raw : []byte) -> string =
  sub("\n", "", base64_encode(raw))

Building the message given the ciphertext and other input parameters is just string concatenation:

format(pub : []byte, nonce : []byte, ct : []byte) -> string =
  "EJ[1:" + encode(pub) + ":" + encode(nonce) + ":" + encode(ct) + "]"

During encryption, an ephemeral keypair is generated and the public key is embedded in the encrypted message.

The final encryption routine combines accepts a plaintext string and a hex-encoded public key extracted from the input document, returning a formatted ecfg message. The NaCl API calls here are loosely paraphrased.

encrypt(plaintext : string, peer_pub_hex : string) -> string =
  peer_pub = base16_to_raw(peer_pub_hex)

  (ephemeral_pub, ephemeral_priv) = NACL.crypto_box_keypair()

  # 24 random bytes
  nonce = NACL.randombytes(NACL.NONCE_BYTES)

  # API here varies a lot depending on binding.
  ciphertext = NACL.crypto_box(
    plaintext,
    ephemeral_priv, peer_pub, nonce
  )

  format(ephemeral_pub, nonce, ciphertext)

If multiple values are being encrypted at once, a single ephemeral keypair may be reused. It may make sense but is by no means necessary to use box precomputation if it's available.

DECRYPTION ALGORITHMS

To decrypt messages from a document, the caller must first retrieve the private key associated to the public key embedded in the document, then the message must be decomposed into the three encoded values. This is just the inverse of the process from the encryption section above: remove the "EJ[]" enclosure; split the message on ":", check that the version is 1, then base64-decode the remaining three components.

Given those three components (peer_pubkey, nonce, and ciphertext), the decryption routine looks like:

decrypt(
  target_privkey : string,
  peer_pubkey    : []byte,
  nonce          : []byte,
  ciphertext     : []byte,
) -> string =
  priv = base16_to_raw(target_privkey)

  # like above, this API varies a lot by binding implementation.
  NACL.crypto_box_open(
    priv, peer_pubkey, nonce, ciphertext
  )

SEE ALSO

ecfg(1), ecfg-encrypt(1), ecfg-decrypt(1), ecfg-keygen(1)

  1. Shopify
  2. August 2016
  3. ecfg(5)