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.