Flightbox CLI Tutorial

CLI overview

The flightbox command-line utility provides subcommands for end-to-end workflows around cryptainers.

$ flightbox -h
Usage: flightbox [OPTIONS] COMMAND [ARGS]...

  Flexible cryptographic toolkit for multi-tenant encryption and signature

Options:
  -v, --verbosity LVL             Either CRITICAL, ERROR, WARNING, INFO or
                                  DEBUG
  -k, --keystore-pool DIRECTORY   Folder tree to store keystores (else
                                  ~/.witnessangel/keystore_pool is used)
  -c, --cryptainer-storage DIRECTORY
                                  Folder to store cryptainers (else
                                  ~/.witnessangel/cryptainers is used)
  -g, --gateway-url TEXT          URL of the web registry endpoint
  -h, --help                      Show this message and exit.

Commands:
  authenticator     Manage authenticator trustees
  cryptainer        Manage encrypted containers
  cryptoconf        Manage cryptographic configurations
  encrypt           Turn a media file into a secure container
  foreign-keystore  Manage locally imported keystores

Note the keystore-pool and cryptainer-storage options: the first one is used to store local and imported keystores, and the second one is used to store encrypted containers.

Hint

The positioning of CLI options is rather strict: they must be added directly after the command they are related too, and before the following subcommand. So for example --gateway-url must be provided after flightbox, but before subcommands like cryptainer, encrypt etc.

Playing with default encryption

Imagine that we won't to encrypt a readme file. The corresponding command could be as simple as:

$ flightbox encrypt readme.rst
warning: No cryptoconf provided, defaulting to simple and INSECURE example cryptoconf
Data file 'readme.rst' successfully encrypted into storage cryptainer

But as the output mentions, this is not a satisfying encryption. Let's see why.

The encrypted container has well been saved into the Cryptainer Storage:

$ flightbox cryptainer list
+------------------+--------+-----------+------------------+
|       Name       |  Size  | Offloaded | Created at (UTC) |
+------------------+--------+-----------+------------------+
| readme.rst.crypt | 102 KB |     X     | 2024-03-30 21:15 |
+------------------+--------+-----------+------------------+

Hint

For performance reasons, the cryptainer is, by default, "offloaded". This means it is separated in two files: the metadata in "readme.rst.crypt", and the (possibly huge) encrypted data in "readme.rst.crypt.payload".

If you open readme.rst.crypt with a text editor, you'll notice that it's just a JSON file, but in Pymongo's Extended Json format: it uses specific fields like $binary or $date to add better types to the serialized data.

Let's now check the structure of this cryptainer:

$ flightbox cryptainer summarize readme.rst.crypt
Loading cryptainer readme.rst.crypt from storage (include_payload_ciphertext=True)

Data encryption layer 1: AES_CBC
  Key encryption layers:
    RSA_OAEP via trustee 'local device'
    Shared secret with threshold 1:
      Shard 1 encryption layers:
        RSA_OAEP via trustee 'local device'
      Shard 2 encryption layers:
        RSA_OAEP via trustee 'local device'
  Signatures:
    SHA256/DSA_DSS via trustee 'local device'

As we see, this cryptainer uses several types of encryption, but only relies on autogenerated local-device keys, which are not protected by a passphrase.

This means that we can directly decrypt the content of this cryptainer:

$ flightbox cryptainer decrypt readme.rst.crypt -o readme.rst.decrypted
Loading cryptainer readme.rst.crypt from storage (include_payload_ciphertext=True)
Decryption report:
[{'entry_criticity': 'INFO',
  'entry_exception': None,
  'entry_message': 'Skipping retrieval of remotely predecrypted symkeys '
                   '(requires requestor-uid and gateway urls)',
  'entry_nesting': 0,
  'entry_type': 'INFORMATION'},
 {'entry_criticity': 'INFO',
  'entry_exception': None,
  'entry_message': 'Starting decryption of payload cipher layer 1/1 (algo: '
                   'AES_CBC)',
  'entry_nesting': 0,
  'entry_type': 'INFORMATION'},
 {'entry_criticity': 'INFO',
  'entry_exception': None,
  'entry_message': 'Decrypting AES_CBC symmetric key through 2 cipher layer(s)',
  'entry_nesting': 1,
  'entry_type': 'INFORMATION'},
 {'entry_criticity': 'INFO',
  'entry_exception': None,
  'entry_message': 'Deciphering 2 shards of shared secret (threshold: 1)',
  'entry_nesting': 1,
  'entry_type': 'INFORMATION'},
 {'entry_criticity': 'INFO',
  'entry_exception': None,
  'entry_message': 'Decrypting shard #1 through 1 cipher layer(s)',
  'entry_nesting': 2,
  'entry_type': 'INFORMATION'},
 {'entry_criticity': 'INFO',
  'entry_exception': None,
  'entry_message': 'Attempting to decrypt key with asymmetric algorithm '
                   'RSA_OAEP (trustee: local_keyfactory)',
  'entry_nesting': 2,
  'entry_type': 'INFORMATION'},
 {'entry_criticity': 'INFO',
  'entry_exception': None,
  'entry_message': 'No predecrypted symmetric found (e.g. coming from remote '
                   'trustee)',
  'entry_nesting': 3,
  'entry_type': 'INFORMATION'},
 {'entry_criticity': 'INFO',
  'entry_exception': None,
  'entry_message': 'Attempting actual decryption of key using asymmetric '
                   'algorithm RSA_OAEP (via trustee)',
  'entry_nesting': 3,
  'entry_type': 'INFORMATION'},
 {'entry_criticity': 'INFO',
  'entry_exception': None,
  'entry_message': 'A sufficient number of shared-secret shards (1) have been '
                   'decrypted',
  'entry_nesting': 2,
  'entry_type': 'INFORMATION'},
 {'entry_criticity': 'INFO',
  'entry_exception': None,
  'entry_message': 'Attempting to decrypt key with asymmetric algorithm '
                   'RSA_OAEP (trustee: local_keyfactory)',
  'entry_nesting': 1,
  'entry_type': 'INFORMATION'},
 {'entry_criticity': 'INFO',
  'entry_exception': None,
  'entry_message': 'No predecrypted symmetric found (e.g. coming from remote '
                   'trustee)',
  'entry_nesting': 2,
  'entry_type': 'INFORMATION'},
 {'entry_criticity': 'INFO',
  'entry_exception': None,
  'entry_message': 'Attempting actual decryption of key using asymmetric '
                   'algorithm RSA_OAEP (via trustee)',
  'entry_nesting': 2,
  'entry_type': 'INFORMATION'}]
Decryption of cryptainer 'readme.rst.crypt' to file 'readme.rst.decrypted' successfully finished

We can then check that readme.rst and readme.rst.decrypted are well the same.

Creating an authenticator trustee

To encrypt data in a more secure fashion, we'll need some key guardians, called trustees in Flightbox.

The simplest form of trustee is an authenticator, a digital identity for a single person. Currently, it is backed by a keystore folder containing some metadata and a bunch of keypairs - all protected by the same "passphrase" (a very long password).

The standard way of generating this identity would be to use a standalone program like the mobile application Witness Angel Authenticator (for Android and iOS), and then to publish the public part of this identity to a web registry.

But we can also create authenticators via the CLI:

$ flightbox authenticator create ./mysphinxdocauthenticator --owner "John Doe" --passphrase-hint "Some hint"
Using passphrase specified as WA_PASSPHRASE environment variable
Authenticator successfully initialized with 3 keypairs in directory /home/docs/checkouts/readthedocs.org/user_builds/witness-angel-cryptolib/checkouts/latest/docs/mysphinxdocauthenticator

For the needs of this doc generation, we had to provide the passphrase as an environment variable, but normally the program will just prompt the user for it.

We can then review the just-created authenticator:

$ flightbox authenticator view ./mysphinxdocauthenticator
+----------------------------+-----------------------------------------------+
|          Property          |                     Value                     |
+----------------------------+-----------------------------------------------+
|        keystore_uid        |      0f91ac29-22bf-d72b-1430-9240d700f928     |
|       keystore_owner       |                    John Doe                   |
|  keystore_passphrase_hint  |                   Some hint                   |
| keystore_creation_datetime |                2024-03-30 21:15               |
|    keypair_identifiers     | RSA_OAEP 0f91ac29-3d7b-41b6-64f1-7633e3bfc8a0 |
|                            | RSA_OAEP 0f91ac29-5ea1-a478-f5e3-7b698c66b40d |
|                            | RSA_OAEP 0f91ac29-d532-b18d-d2e5-8ac4ef902587 |
+----------------------------+-----------------------------------------------+

Like for most list/view commands, we can switch to JSON format for automation purposes:

$ flightbox authenticator view ./mysphinxdocauthenticator --format json
{
  "keypair_identifiers": [
    {
      "key_algo": "RSA_OAEP",
      "keychain_uid": {
        "$binary": {
          "base64": "D5GsKT17QbZk8XYz47/IoA==",
          "subType": "04"
        }
      },
      "private_key_present": true
    },
    {
      "key_algo": "RSA_OAEP",
      "keychain_uid": {
        "$binary": {
          "base64": "D5GsKV6hpHj143tpjGa0DQ==",
          "subType": "04"
        }
      },
      "private_key_present": true
    },
    {
      "key_algo": "RSA_OAEP",
      "keychain_uid": {
        "$binary": {
          "base64": "D5GsKdUysY3S5YrE75Alhw==",
          "subType": "04"
        }
      },
      "private_key_present": true
    }
  ],
  "keystore_creation_datetime": {
    "$date": {
      "$numberLong": "1711833305158"
    }
  },
  "keystore_owner": "John Doe",
  "keystore_passphrase_hint": "Some hint",
  "keystore_uid": {
    "$binary": {
      "base64": "D5GsKSK/1ysUMJJA1wD5KA==",
      "subType": "04"
    }
  }
}

We can check, later, that we still remember the right passphrase for this authenticator:

$ flightbox authenticator validate ./mysphinxdocauthenticator
Using passphrase specified as WA_PASSPHRASE environment variable
Authenticator has UID 0f91ac29-22bf-d72b-1430-9240d700f928 and belongs to owner John Doe
Creation date: 2024-03-30 21:15:05+00:00
Keypair count: 3

Authenticator successfully checked, no integrity errors found

We can delete the authenticator with flightbox authenticator delete ./mysphinxdocauthenticator, which is the same as manually deleting the folder.

Importing foreign keystores

Authenticators are supposed to be remote identities, well protected by their owner. To use them in our encryption system, we need to import their public keys, which are like "padlocks". That's what we call "foreign keystores" - partial local copies of remote identities.

Let's begin by importing the authenticator we just created.

$ flightbox foreign-keystore import --from-path ./mysphinxdocauthenticator
Importing foreign keystore from folder /home/docs/checkouts/readthedocs.org/user_builds/witness-angel-cryptolib/checkouts/latest/docs/mysphinxdocauthenticator, without private keys
Authenticator 0f91ac29-22bf-d72b-1430-9240d700f928 (owner: John Doe) imported

Let's also import an identity from a web registry, using its UUID that the owner gave us directly.

$ flightbox --gateway-url https://api.witnessangel.com/gateway/jsonrpc/ foreign-keystore import --from-gateway 0f0c0988-80c1-9362-11c1-b06909a3a53c
Importing foreign keystore 0f0c0988-80c1-9362-11c1-b06909a3a53c from web gateway
Initiating remote call 'get_public_authenticator()' to server https://api.witnessangel.com/gateway/jsonrpc/
Authenticator 0f0c0988-80c1-9362-11c1-b06909a3a53c (owner: ¤aaa) imported

If we have setup authenticators in default locations of connected USB keys, we can automatically import them:

$ flightbox foreign-keystore import --from-usb --include-private-keys
Importing foreign keystores from USB devices, with private keys
0 new authenticators imported, 0 updated, 0 skipped because corrupted

Warning

The --include-private-keys option requests that the private part of the identity be imported too, if present (which is not the case e.g. for web gateway identities). This is only useful if one intends to decrypt data locally, by entering passphrases during decryption. But much more secure workflows are now available, for example by using the mobile application Authenticator.

We can then review the imported keystores, which will be usable for encryption:

$ flightbox foreign-keystore list
+--------------------------------------+----------+-------------+--------------+------------------+
|             Keystore UID             |  Owner   | Public keys | Private Keys | Created at (UTC) |
+--------------------------------------+----------+-------------+--------------+------------------+
| 0f0c0988-80c1-9362-11c1-b06909a3a53c |   ¤aaa   |      7      |      0       |                  |
| 0f91ac29-22bf-d72b-1430-9240d700f928 | John Doe |      3      |      0       | 2024-03-30 21:15 |
+--------------------------------------+----------+-------------+--------------+------------------+

And we can check the keypairs present in a specific keystore, this way:

$ flightbox foreign-keystore view 0f0c0988-80c1-9362-11c1-b06909a3a53c
+----------------------------+-----------------------------------------------+
|          Property          |                     Value                     |
+----------------------------+-----------------------------------------------+
|        keystore_uid        |      0f0c0988-80c1-9362-11c1-b06909a3a53c     |
|       keystore_owner       |                      ¤aaa                     |
| keystore_creation_datetime |                                               |
|    keypair_identifiers     | RSA_OAEP 0f0c0989-1111-a226-c471-99cbb2d203c3 |
|                            | RSA_OAEP 0f0c0989-a4fa-7c15-0b07-932729d9dc5b |
|                            | RSA_OAEP 0f0c0989-e6ae-f533-d92a-cc2dc651acb8 |
|                            | RSA_OAEP 0f0c098b-5d2f-633c-167b-a8d5c86067ff |
|                            | RSA_OAEP 0f0c098c-47a6-6a2e-7071-f28f97c3093a |
|                            | RSA_OAEP 0f0c098c-ce02-7828-a519-45e6df84a25f |
|                            | RSA_OAEP 0f0c098d-59a3-1061-92a0-fcd9549a7dac |
+----------------------------+-----------------------------------------------+

We can later delete the foreign keystore with flightbox foreign-keystore delete 0f0c0988-80c1-9362-11c1-b06909a3a53c, which is the same as manually deleting the folder deep inside the keystore pool.

Generating simple cryptoconfs

Now that we have locally registered some trustees, it's time to specify how they should protect our data, how they should become our "key guardians". This happens with a cryptoconf, a JSON cryptainer template recursively describing the different layers of encryption to be used on data and on keys, as well as the signatures to apply.

Cryptoconf can be very complex; but for some low-depth, signatureless cases, we can use the CLI to generate a cryptoconf for us.

For example, imagine we want to encrypt the data using the AES-CBC cipher, and then protect the (random) secret key of this cipher using a keypair of the trustee imported from the web gateway.

$ flightbox cryptoconf generate-simple add-payload-cipher-layer --sym-cipher-algo AES_CBC add-key-cipher-layer --asym-cipher-algo RSA_OAEP --trustee-type authenticator --keystore-uid 0f0c0988-80c1-9362-11c1-b06909a3a53c --keychain-uid 0f0c0989-1111-a226-c471-99cbb2d203c3
{
  "payload_cipher_layers": [
    {
      "key_cipher_layers": [
        {
          "key_cipher_algo": "RSA_OAEP",
          "key_cipher_trustee": {
            "keystore_uid": {
              "$binary": {
                "base64": "DwwJiIDBk2IRwbBpCaOlPA==",
                "subType": "04"
              }
            },
            "trustee_type": "authenticator"
          },
          "keychain_uid": {
            "$binary": {
              "base64": "DwwJiRERoibEcZnLstIDww==",
              "subType": "04"
            }
          }
        }
      ],
      "payload_cipher_algo": "AES_CBC",
      "payload_signatures": []
    }
  ]
}

The UUIDs that we selected are well there, even if unrecognizable in the $binary/base64 format of the JSON.

Hint

What are the keychain UIDs added above?

They uniquely identify a keypair for a given algorithm and trustee.

Each cryptainer has a root keychain UID, autogenerated if not provided by the cryptoconf. This default keychain UID is sufficient for the local-keyfactory trustee, since it will generate new keypairs on demand. But for each authenticator trustees, the set of available keypairs is already determined; so we must choose the keypair that we want to rely on, by providing its keychain UID at trustee level.

We can go farther, and decide that we want two layers of data encryption:

  • one protected by an autogenerated local key

  • the other protected by a shared secret between two authenticators, any of these two being sufficient to decrypt the data.

Here is how such a configuration could be generated:

$ flightbox cryptoconf generate-simple
    add-payload-cipher-layer --sym-cipher-algo AES_CBC
        add-key-cipher-layer --asym-cipher-algo RSA_OAEP --trustee-type local_keyfactory
    add-payload-cipher-layer --sym-cipher-algo CHACHA20_POLY1305
        add-key-shared-secret --threshold 1
            add-key-shard --asym-cipher-algo RSA_OAEP --trustee-type authenticator --keystore-uid 0f0c0988-80c1-9362-11c1-b06909a3a53c --keychain-uid 0f0c0989-1111-a226-c471-99cbb2d203c3
            add-key-shard --asym-cipher-algo RSA_OAEP --trustee-type authenticator --keystore-uid 7a25db2c-4c4e-42bb-a064-8da2007a4fd7 --keychain-uid 8c57e283-308a-4c78-86f9-ee6176757a6f
    > shared-secret-cryptoconf.json``

And here is the resulting cryptoconf structure:

$ flightbox cryptoconf summarize sophisticated-cryptoconf.json

Data encryption layer 1: AES_CBC
  Key encryption layers:
    RSA_OAEP via trustee 'local device'
  Signatures: None
Data encryption layer 2: CHACHA20_POLY1305
  Key encryption layers:
    Shared secret with threshold 1:
      Shard 1 encryption layers:
        RSA_OAEP via trustee 'authenticator 0f0c0988-80c1-9362-11c1-b06909a3a53c'
      Shard 2 encryption layers:
        RSA_OAEP via trustee 'authenticator 7a25db2c-4c4e-42bb-a064-8da2007a4fd7'
  Signatures: None

If we want a logical AND instead of a logical OR between the two authenticator-based trustees, either we increase the threshold to 2, or we apply the trustee protections one after the other, like this:

$ flightbox cryptoconf generate-simple add-payload-cipher-layer --sym-cipher-algo AES_CBC
    add-key-cipher-layer --asym-cipher-algo RSA_OAEP --trustee-type authenticator --keystore-uid 0f0c0988-80c1-9362-11c1-b06909a3a53c --keychain-uid 0f0c0989-1111-a226-c471-99cbb2d203c3 --sym-cipher-algo AES_EAX
    add-key-cipher-layer --asym-cipher-algo RSA_OAEP --trustee-type authenticator --keystore-uid 7a25db2c-4c4e-42bb-a064-8da2007a4fd7 --keychain-uid 8c57e283-308a-4c78-86f9-ee6176757a6f
    > multikeylayer-cryptoconf.json

Which gives this structure:

$ flightbox cryptoconf summarize multikeylayer-cryptoconf.json

Data encryption layer 1: AES_CBC
  Key encryption layers:
    AES_EAX with subkey encryption layers:
      RSA_OAEP via trustee 'authenticator 0f0c0988-80c1-9362-11c1-b06909a3a53c'
    RSA_OAEP via trustee 'authenticator 7a25db2c-4c4e-42bb-a064-8da2007a4fd7'
  Signatures: None

Thus, the randomly generated AES-CBC is secured by the first trustee, and then the result of this encryption is fed to the second trustee, which secures it too.

Hint

Note that we used an hybrid encryption (AES-EAX/RSA-OAEP) for the first layer of key encryption; this is not mandatory, but it avoid stacking trustees directly one over the other in these "Key encryption layers".

When trustees are directly stacked, decryption is complicated because we must decrypt the Key through the upper layer, before being able to query the next trustee for authorization, using the now partially-decrypted Key.

When trustees are separated "leaves" of the cryptoconf/cryptainer tree, on the contrary, they can all be queried in parallel for authorizations, each one being fed its corresponding encrypted "Key" (here, respectively the encrypted AES-CBC and AES-EAX keys).

Note that a flightbox cryptoconf validate <file> command is available, to check JSON cryptoconfs that you have generated by other means.

We haven't evocated, in this tutorial, the "server-backed" trustees (trustee_type "jsonrpc_api" in cryptoconfs). They can be used as encryption/signature keypair providers, but the recorder device must be constantly connected to Internet, and their current decryption workflow offers low security, so we'd rather not use them for now.

Securely encrypting data

Now that we have dealt with trustees and cryptoconf, the rest is easy:

$ flightbox encrypt readme.rst --cryptoconf cryptoconf.json -o readme
Data file 'readme' successfully encrypted into storage cryptainer

Notice that we can choose the basename of the target cryptainer with -o. There is also a --bundle option to output the cryptainer as a single file - handy if the input file is rather enough.

Managing cryptainers

The commands to list, summarize, validate, and delete cryptainers from the current "Cryptainer Storage" are quite straightforward:

$ flightbox cryptainer -h
Usage: flightbox cryptainer [OPTIONS] COMMAND [ARGS]...

  Manage encrypted containers

Options:
  -h, --help  Show this message and exit.

Commands:
  decrypt    Turn a cryptainer back into the original media file
  delete     Delete a local cryptainer
  list       List local cryptainers
  purge      Delete oldest cryptainers per criteria
  summarize  Display a summary of a cryptainer structure
  validate   Validate a cryptainer structure

The purge command can combine multiple criteria to ensure that technical and legal constraints are met. For example if we can only keep the cryptainers 30 days, and want to limit their count to 100 and their total space to 1000 MB, we can run:

$ flightbox cryptainer purge --max-age 30 --max-count 100 --max-quota 1000
Intentionally purging cryptainers
Cryptainers successfully deleted: 0

Finally, the decrypt command is not relevant for our new cryptoconfs, since it doesn't support the complex mix of passphrases and remote authorization requests necessary to reveal a Flightbox cryptainer. It's better to use some Revelation Station software, like that included in the W.A Recorder program of Witness Angel project.