✂️ Split encryption keys and encrypt key shares with different age (licensed BSD 3-Clause) recipients.
🛂 Create (complex) policies about which combinations of identites can decrypt the file.
📌 Uses Hashicorp Vault's implementation of Shamir's Secret Sharing (licensed MPL-2.0).
📝 See SPEC.md for more details.
Download a the latest binary from the release page. Copy the binary to your $PATH
(preferably in $(which age)
) and make sure it's executable.
You can also use the following script for installation:
- Installs binary to
~/.local/bin/age-plugin-sss
(change to your preferred directory) - Make sure to adjust
OS
andARCH
if needed (OS=darwin ARCH=arm64
for Apple Silicon,OS=darwin ARCH=amd64
for older Macs)
cd "$(mktemp -d)"
VERSION=v0.2.5 OS=linux ARCH=amd64; curl -L "https://github.com/olastor/age-plugin-sss/releases/download/$VERSION/age-plugin-sss-$VERSION-$OS-$ARCH.tar.gz" -o age-plugin-sss.tar.gz
tar -xzf age-plugin-sss.tar.gz
mv age-plugin-sss/age-plugin-sss ~/.local/bin
Please note that Windows builds are currently not enabled, but if you need them please open a new issue and I'll try to look into it.
Alternatively the plugin can be built from source. Simply run
go build -v ./cmd/...
and copy the binary age-plugin-sss
to a directory of your $PATH
.
First, define a policy in YAML format:
threshold: 2
shares:
- age1t7cexdfjmkk4fgsf6pgzhn0skk0qewxr9y7tdu3l639fdmptcaxqv3nznt
- age1jcq99v6f74gwstqhg2vsll5s3rckdys8ttr2nnrzpegxu0y533vqnf7d2u
- age1zunvd6ztdeljcxzhe70370cx5q54czyhy2qjgsnju9rsyjaexqqqfrxg2w
In this case, the file key will be split into three shares, each of which will be encrypted with one of the three recipients in the shares
array. The threshold
specifies how many of the shares are required to decrypt the file again. For our example, any two identities that correspond to a recipient in the list will suffice.
You can not only use native age recipients (see below how to handle passwords), but also plugin or SSH recipients.
Next, generate a new recipient from this policy file:
$ age-plugin-sss --generate-recipient policy.yaml
age1sss1r79ssqqqqqqqqq8l2nxy6m5yyq2qpc9mkz0q2pv4uf2e5tsv0urpec5rqupvdwmhm8xqt05mypvanzmyktldcfy3j4kvul9p2znxn67ly9xdvedmn3hwey0xzq5f32e9myz74s50s496hhe842k5ret3gsjvl6ul7y92ftytzkfmkvkzaevvm3e3v709f5pa0u3jv9nrr2nhrtws8ee4ug2659vljczx392j7qa48x7x5cehsfeyz4vmvx0df6rmvls9mr47ez2thh6vqvqfhxnrzauha8alqqqqplll80huf5hzqqqqq3csvcr
As you can see, recipients for this plugin can be quite long because they contain multiple other recipients used for encrypting key shares.
Now you can use this recipient to encrypt anything, e.g.
echo "it works" | age -r age1sss1r79ssqqqqqqqqq8l2nxy6m5yyq2qpc9mkz0q2pv4uf2e5tsv0urpec5rqupvdwmhm8xqt05mypvanzmyktldcfy3j4kvul9p2znxn67ly9xdvedmn3hwey0xzq5f32e9myz74s50s496hhe842k5ret3gsjvl6ul7y92ftytzkfmkvkzaevvm3e3v709f5pa0u3jv9nrr2nhrtws8ee4ug2659vljczx392j7qa48x7x5cehsfeyz4vmvx0df6rmvls9mr47ez2thh6vqvqfhxnrzauha8alqqqqplll80huf5hzqqqqq3csvcr -o testing.enc
First, define a new YAML file containing the identities to use for decryption with the following structure:
identities:
- AGE-SECRET-KEY-1XUR5...
- AGE-SECRET-KEY-17FXT...
This file only contains a flat list of identities. The structure of the policy (or order of shares) is not replicated here and thresholds or other information do not need to be included. There must be enough valid identities to meet the secret splitting threshold, though.
Next, generate an identity from the YAML file (similarly as done for recipients):
age-plugin-sss --generate-identity example-id.yaml > id.txt
Finally, the encrypted file can be decrypted with the generated identity:
$ age -d -i id.txt testing.enc
it works
Let's say you want to encrypt a file so that in addition to your X25519 identity you also need one of two fido2 keys for decryption. This can be achived by adding a nested policy in one of the shares as such:
threshold: 2
shares:
- age1t7cexdfjmkk4fgsf6pgzhn0skk0qewxr9y7tdu3l639fdmptcaxqv3nznt
- threshold: 1
shares:
- age1fido2-hmac1qqa...
- age1fido2-hmac1qqb...
You can add as many nested level as you want. Please notice, however, that for decryption the (sss-)plugin might ask you which share to decrypt in the case of plugin recipients/identities interactively and that the recipient string encoding the policy will be rather long.
You can bypass the user interaction for selecting the share if the plugin is unsure about which one to choose by specifying a share_id
in your identity file. To find out which share in the policy tree you want to target, you can inspect the policy of the encrypted file with the following command:
$ age-plugin-sss --inspect test.enc
sss (t=2)
├─ x25519 [id=1]
└─ sss (t=1)
├─ fido2-hmac [id=2]
└─ fido2-hmac [id=3]
For example, you may only want to use one of your fido2 tokens for decryption because the other one is only a backup. In your identity YAML file, simply pin the share_id
to the correct one:
identities:
- AGE-SECRET-KEY-1XUR5...
- share_id: 3
identity: AGE-PLUGIN-FIDO2-HMAC-1VE5KGMEJ945X6CTRM2TF76
It's also possible to include an SSH public key in a policy and an SSH private key in an identity. Make sure to use YAML's multi-line string formatting for the identity.
Example:
# recipient.yaml
threshold: 1
shares:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL84xOFSWXIcAeQK8CJ0qvHojdFZDuLGRe5FPg4aM3kY testing@local
# identity.yaml
identities:
- |
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACC/OMThUllyHAHkCvAidKrx6I3RWQ7ixkXuRT4OGjN5GAAAAKCxGCybsRgs
mwAAAAtzc2gtZWQyNTUxOQAAACC/OMThUllyHAHkCvAidKrx6I3RWQ7ixkXuRT4OGjN5GA
AAAEBqlzBxbT+cd7xs19UN6ZFKG2bb4vtoR6/7FHt7yJ4DZ784xOFSWXIcAeQK8CJ0qvHo
jdFZDuLGRe5FPg4aM3kYAAAAGnNlYmFzdGlhbkBmZWRvcmEuZnJpdHouYm94AQID
-----END OPENSSH PRIVATE KEY-----
If the private key is passphrase-protected, you will be prompted for the password, but only if the encrypted private key contains the public key. Otherwise, you'll need to create a version of your private key that is not passphrase encrypted (see here). This issue is tracked in #3.
Let's say you want to split the encryption key into shares wrapped by different passwords. As there is no recipient string you can generate, you must provide a special keyword of the form password-<name-or-slug>
, e.g.
threshold: 3
shares:
- password-alice
- password-bob
- password-chris
The plugin will ask you for each password upon encryption. Please notice that
- the name/slug is required, but it is not persisted in the encrypted file. It's only there for you to not confuse passwords when interacting with the cli.
- the name/slug is mandatory for encryption, but optional for decryption (as the plugin will try all password stanzas until it finds the correct one). So having one or multiple identities named
password
is fine.
You can always decode a recipient or identity with the --decode
flag by passing the string via STDIN:
$ cat recipient.txt | age-plugin-sss --decode
threshold: 2
shares:
- recipient: age1t7cexdfjmkk4fgsf6pgzhn0skk0qewxr9y7tdu3l639fdmptcaxqv3nznt
- recipient: age1jcq99v6f74gwstqhg2vsll5s3rckdys8ttr2nnrzpegxu0y533vqnf7d2u
- recipient: age1zunvd6ztdeljcxzhe70370cx5q54czyhy2qjgsnju9rsyjaexqqqfrxg2w
It's totally valid to define the same recipient more than once anywhere inside a policy. However, when decrypting you must list the same identity multiple times, as well. This is because with the current design the plugin assumes that one identity only unwraps one share. For plugin identities, you may also consider specifying the share_id
in your identity YAML file.