Merge pull request #1 from mozilla-services/0.1

0.1 prototype
This commit is contained in:
Julien Vehent 2015-09-03 09:26:08 -04:00
Родитель 4e8d2bb5d9 3330208362
Коммит a5e0899de8
4 изменённых файлов: 584 добавлений и 0 удалений

Просмотреть файл

@ -1,2 +1,140 @@
SOPS: Secrets OPerationS
========================
`sops` is a cli that encrypt values of yaml, json or text files using AWS KMS.
Usage
-----
Editing
~~~~~~~
`sops` encrypted file contain the necessary KMS information to decrypt their
content. All a user of `sops` need is valid AWS credentials and the necessary
permissions on KMS keys.
Given that, the only command a `sops` user need is:
.. code:: bash
$ sops <file>
`<file>` will be opened, decrypted, passed to a text editor (vim by default),
encrypted if modified, and save back to its original location. All of these
steps, apart from the actual editing, are transparent to the user.
Creating
~~~~~~~~
In order to create a file, the KMS ARN must be provided to `sops`, either on the
command line in the `-k` flag, or in the environment variable **SOPS_KMS_ARN**.
`sops` automatically create a file if the given path doesn't exist (it will not
create folders, however).
.. code:: bash
$ sops newfile.yaml -k arn:aws:kms:us-east-1:656532927350:key/920aff2e-c5f1-4040-943a-047fa387b27e
newfile.yaml doesn't exist, creating it.
new data key generated from kms: CiC6yCOtzsnFhkfdIs...
file written to newfile.yaml
Input some cleartext yaml:
.. code:: yaml
myapp1: t00m4nys3cr3tz
app2:
db:
user: bob
password: c4r1b0u
# private key for secret operations in app2
key: |
-----BEGIN RSA PRIVATE KEY-----
MIIBPAIBAAJBAPTMNIyHuZtpLYc7VsHQtwOkWYobkUblmHWRmbXzlAX6K8tMf3Wf
Erb0xAEyVV7e8J0CIQC8VBY8f8yg+Y7Kxbw4zDYGyb3KkXL10YorpeuZR4LuQQIg
bKGPkMM4w5blyE1tqGN0T7sJwEx+EUOgacRNqM2ljVA=
-----END RSA PRIVATE KEY-----
an_array:
- secretuser1 # a super secret user
- secretuser2
sops:
kms:
enc: CiC6yCOtzsnFhkfdIslYZ0bAf//gYLYCmIu87B3sy/5yYxKnAQEBAQB4usg......
enc_ts: 1439587921.752637
arn: arn:aws:kms:us-east-1:656532927350:key/920aff2e-c5f1-4040-943a-047fa387b27e
After saving the file and exiting, it is automatically encrypted. Keys are
still in cleartext, but value are now unreadable.
.. code:: yaml
myapp1: ENC[AES256_GCM,data=s4mlbkPqyk+GFDluAHY=,iv=7c9X8CwZyK5PsRRmUpzxL4CeQmp7+ry6mVemJtmpR7U=,aad=CFVNHUiz8xupOCMNYUlF4l+TcCjGaxayiknL9tQtolw=,tag=5ecBRedoXPJJ3uBjaj7J1w==]
app2:
db:
user: ENC[AES256_GCM,data=CmkT,iv=xnUTxXU4g5lKEqetiZrM2s+m20idUUt9xGU6XitsIic=,aad=KidFJD6ioPXKqz+BYVYXtHk8Dd6e1yvhPx6kO5BOJTs=,tag=7WDZbBf2oqMuXi3YH4m2Ig==]
password: ENC[AES256_GCM,data=zw2yh6Oz8Q==,iv=Apme9l8h+OwdwgbozsuXa1mVK+b821eoQNEBBSF6Ihs=,aad=SZFoaQDlNe0SkRaX65zB7E8SDyhkr9uVBI+3GWUBKsQ=,tag=We5dwW455S1M4ob1HzAu7Q==]
# private key for secret operations in app2
key: |-
ENC[AES256_GCM,data=feo0o1qW8p4Nw2tN9/QAt0zoeGZHgolORWXH+7hk4Oc5nQcA/Ve3mYQ9TKSZAtzsYr+OEnEVUAg/RzvXy40F9dXsv2ugux+DLS1SIWddKRAdeL283vjnsDtydc3+AP+UuEuCyIVHqT8uKcqQnenzzu/yx07scIwcMQ6Vs2RnQ3WwrOrkBsbPQ4PpuPsrlck7EbcKkMnoIe09AMN3/J3A+mlmOGBxAio1ahFXpeBzzYeoRkjffojvigT2ULZy92Kx1afRSnWXNmUtMKqbJDIIvYulWHW5efAnulk/nHZ0Bhy+wxV0jqXAp7mKiIlGuydxRZ6DPon7jhABWV5d93EZdZJ+/33sUiOyQKIEukqae17C2Hrt0QoGg7OhG/O0oyTKiql0Nj6KC3bFbkdM7sSFbsIbv/of0P5Kb2zr3VYAJriJqUWMKzj3i7M6z9+wxrTVxuMQ4Lvzw3aHDjNOgJobkfjxxMYUvF5l2OWRFrdxtY9WxBYAcDzkJnBYtkPnUzlEc/8ieypefqOBlcphOvzl+EjM1I0N4OGG5ij5nNsHQ/MSoM3FJpjROQKclhz8ZN5CH41LUemP3AdddPpoUuwzHCxR8NskUhyHBlep0iZL9xGFLL7SwYEACKxk2BCwHMWeNmXfKo6co+wjCmn+un3FANE=,iv=NworRcR7VnLgW30c4W9OmVgBaY7tA1fd090JQpBM5ho=,aad=sbwFbTuEr9FbPd/ofR7BL9NORUpfmNd+X3Q+tJqmj8g=,tag=wc7RWWBArQrTMt3AAbSwZQ==]
an_array:
- ENC[AES256_GCM,data=L3Y0Bzn2M6yERcU=,iv=FslXY0z783MXhjCaz9ZZTqNaEwBWZkspNHAtHJaENH0=,aad=x0x9+PnDW81oLbYufq72RmaRZB29IPCALCL94KtmsvQ=,tag=qPyqJ3I9JM6wIJDOmgmJkQ==]
- ENC[AES256_GCM,data=To5dwUDJi4Mh3hc=,iv=03vcf/AJaUKcHKEnGPq7ih8/xaKHewYiFkQcWOsh7So=,aad=nxUVG7rA+TjyK9BrzVtDGbCp7Iu7BCRLjYvZSnI5iCI=,tag=41ExX9KH+jRYvn51aaP6OA==]
sops:
kms:
enc: CiC6yCOtzsnFhkfdIslYZ0bAf//gYLYCmIu87B3sy/5yYxKnAQEBAQB4usgjrc7JxYZH3SLJWGdGwH//4GC2ApiLvOwd7Mv+cmMAAAB+MHwGCSqGSIb3DQEHBqBvMG0CAQAwaAYJKoZIhvcNAQcBMB4GCWCGSAFlAwQBLjARBAwkRAZG5vQyIKvIKPwCARCAO9zQ43qeQ8loKu0HzXRnpqi6MK/+TpbO22sH0NkVXddXNTl7lfPjKc6gJynrEVdu6aCslUYIid+3FONY
enc_ts: 1439587921.752637
arn: arn:aws:kms:us-east-1:656532927350:key/920aff2e-c5f1-4040-943a-047fa387b27e
To decrypt, using flag `-d`.
.. code:: bash
$ sops -d newfile.yaml
myapp1: t00m4nys3cr3tz
app2:
db:
user: bob
[...]
Set the env variable **SOPS_KMS_ARN** to your KMS ARN value to avoid
needing to set the `-k` flag every time you create a file.
.. code:: bash
$ export SOPS_KMS_ARN="arn:aws:kms:us-east-1:656532927350:key/920aff2e-c5f1-4040-943a-047fa387b27e"
$ sops newfile.yaml
Requirements
------------
* `boto3 <https://pypi.python.org/pypi/boto3/1.1.1>`_
* `ruamel.yaml <https://pypi.python.org/pypi/ruamel.yaml>`_; requires
libyaml-devel and python-devel prior to `pip install`-ing it.
.. code::
sudo yum install libyaml-devel python-devel
sudo pip install ruamel.yaml
* `cryptography <https://pypi.python.org/pypi/cryptography>`_; requires
libffi-devel prior to `pip install`-ing it.
.. code::
sudo yum install libffi-devel
sudo pip install cryptography
License
-------
Mozilla Public License Version 2.0
Authors
-------
* Julien Vehent
Credits
-------
`sops` is inspired by projects like `hiera-eyaml
<https://github.com/TomPoulton/hiera-eyaml>`_, `credstash
<https://github.com/LuminalOSS/credstash>`_ and `sneaker
<https://github.com/codahale/sneaker>`_.

5
example.txt Normal file
Просмотреть файл

@ -0,0 +1,5 @@
ENC[AES256_GCM,data=2CC7cVfI1TwHUdVoyEtXuyS+h5ZeFRQ7RzTxsUBsXjj1WqXoVxVDqSl8prx/C2wsjW5OBB/YaZr2huUtbAlPxbW2B1B8d6u/0NBZBAC3pJTRYnYF9oOyLoKRHkMkEYhRHjs9yUx9IZvPAY6tnHO8LBh7V+qzFpZ/ZSbbvyIlNEEVymEn7VNNjsozOFiDyaPh7KVK/u6KsLqcYSElVgmVen3IoPimPoD6WytJYvovkoKmbTz+gMpQACrrDk98wRmbl8isMGT/+QhShoYDLqgxqLIzT9Yqf+9H9xnKPMuqITsaC0oq1t4E5Uh653tTgpxUvsIbNzye2bMcKHslldMtcthnQas0QgCSzN0f82WSNTn4v9bf914NFdc1uF+JHfKUnped0u9X0APbiVdxokPKcXr4KQIItfQMKlzoa3D4cjWpx2Lz2l/0pZYrlmuciCznEJXSwcIE/8RhoCvQbR/3ckw/3fhFoFRIshAD6WFSXfm4EUHWbIvEPJ3SfhFAVBws0O5LZEqXryVYEkHteqCfBW5Kh88LSuiWZ5H/3/gDmwpM4xv7Hrxo5I4e/2ZogyySzh71C84ngVUt9jlmDvdo7tzmlEeG6tVktS0HFQaVyCL5tHDYY6Xh7Wg=,iv=IJXjsaYvvEYC6pX4Wec9CS29xHoZ84Y/n0rm5CQJXZY=,aad=7TFp7NRJtNlM10CuQC0RpeULfLxONWGUSiR6tvO8EdY=,tag=SaOeqojH9lHk+XOqyuF6UQ==]
# --- sops encryption info. do not edit. ---
SOPS_KMS_ARN=arn:aws:kms:us-east-1:656532927350:key/920aff2e-c5f1-4040-943a-047fa387b27e
SOPS_KMS_ENC=CiC6yCOtzsnFhkfdIslYZ0bAf//gYLYCmIu87B3sy/5yYxKnAQEBAQB4usgjrc7JxYZH3SLJWGdGwH//4GC2ApiLvOwd7Mv+cmMAAAB+MHwGCSqGSIb3DQEHBqBvMG0CAQAwaAYJKoZIhvcNAQcBMB4GCWCGSAFlAwQBLjARBAwGvcLMWiThqNayEncCARCAO2XIzjbSX1e8HjKcRkSXpZldSkigmO/WHg++c5ee1YarV9zaQB46Vd95baJZxQrGWW89bRKT53Nb/IsN
SOPS_KMS_ENCTS=1439584577.42

17
example.yaml Normal file
Просмотреть файл

@ -0,0 +1,17 @@
myapp1: ENC[AES256_GCM,data=Tr7oc19nc6t1m9OrUeo=,iv=1vzlPZLfy6wa14/x17P8Ix8wEGDeY0v2dIboZmmwpww=,aad=NpobRzMzpDOkqijzONm8KglltzG+aBV7BJAxtm77veo=,tag=kaYqRgGGBhXhODSSmIZwyA==]
app2:
db:
user: ENC[AES256_GCM,data=CwE4O1s=,iv=S0fozGAOxNma/pWDUuk1iEaYw0wlba0VOLHjPxIok2k=,aad=nEVizsMMyBXOxySnOHw/trTFBSW72nh+Q80YU7TPgIo=,tag=XaGsYaL9LCkLWJI0uxnTYw==]
password: ENC[AES256_GCM,data=p673JCgHYw==,iv=EOOeivCp/Fd80xFdMYX0QeZn6orGTK8CeckmipjKqYY=,aad=UAhi/SHK0aCzptnFkFG4dW8Vv1ASg7TDHD6lui9mmKQ=,tag=QE6uuhRx+cGInwSVdmxXzA==]
# private key for secret operations in app2
key: |-
ENC[AES256_GCM,data=Ea3zTFSOlg1PDZmBa1U2dtKl3pO4nTmaFswJx41fPfq3u8O2/Bq1UVfXn2SrO13obfr6xH4zuUceCDTvW2qvphlan5ir609EXt4dE2TEEcjVKhmAHf4LMwlZVAbvTJtlsnvo/aYJH95uctjsSX5h8pBlLaTGBGYwMrZuMyRU6vdcMWyha+piJckUc9sq7fevy1TSqIxf1Usbn/0NEklWm2VSNzQ2Urqtny6EXar+xU7NfYSRJ3mqmcJZ14oIeXPdpk962RwMEFWdYrbE7D59kWU2BgMjDxYJD5KXpWiw2YCrA/wsATxVCbZlwqC+TJFA5WAUZX756mFhV/t2Li3zQyDNUe6KkMXV9qwf/oV1j5sVRVFsKDYIBqhi3qWBVA+SO9RloQMjhru+IsdbQcS4LKq/1DrBENeZuJ0djUAxKLVfJzMGUf89ju3m9IEPovW8mfF0RbfAGRwFHMO9nEXCxrTLERf3owdR3u4j5/rNBpIvvy1z+2dy6sAx/eyNdS+cn5qO9BPAxsXpSwkaI96rlBagwH1Pfxus0x/D00j93OpE+M8MgQ/9LA68FlCFU4OAQlvw8f7MPoxnq+/+gFTS/qqjTR6EoUuX5NH2WY93YCC5TCbe4GOXyP0H05PbIWq55UMVLNcpAyac3gO4kL5O5U8=,iv=Dl61tsemKH0fdmNul/PmEEsRYFAh8GorR8GRupus/EM=,aad=Ft2aSYYukD1x8pMj1WvmodLjJV6waPy5FqdlImWyQKA=,tag=EPg4KpWqni/buCFjFL857A==]
an_array:
- ENC[AES256_GCM,data=v8dfh92oL8IcgjQ=,iv=HgNNPlQh9GNdE+YPvG4Ufpb2I0sIlEpCsOW3lJA1uBE=,aad=21GroP5gb9sCTxZIahN1NhMGqRPQZZksAr5Q7eCeHRc=,tag=gLsjVqot9+Pqck9LJC+bVA==]
- ENC[AES256_GCM,data=X1LMy27AE9SI4h0=,iv=oA1kSg9esGxAvi3qhpcM6Ewrh+p0CFV5cgf6jSPpM08=,aad=CZ7FGJNko6367sd6PwbrIgN/V7Rly4TptbQ1gVsXT1Q=,tag=HerE4nTstX2QZhMn3CPZcw==]
- ENC[AES256_GCM,data=KNkH9iI0bSyvcP3E+BRbqfcPUv3YBbCmtvbK1y+sHMI6Z1kXnkX4RoyYiZZXrM680Nh/p0TxNOdNsA==,iv=1h3KbThwTsRaVF+k+dnSwfocSEoyT00X279Dg1Wro60=,aad=foCwpM862VeAD2/7bHRJHAYISneTUJweoSRl2oAdsI4=,tag=tNuCjsNqIy5FVDRu39dQcw==]
sops:
kms:
enc: CiC6yCOtzsnFhkfdIslYZ0bAf//gYLYCmIu87B3sy/5yYxKnAQEBAQB4usgjrc7JxYZH3SLJWGdGwH//4GC2ApiLvOwd7Mv+cmMAAAB+MHwGCSqGSIb3DQEHBqBvMG0CAQAwaAYJKoZIhvcNAQcBMB4GCWCGSAFlAwQBLjARBAyGdRODuYMHbA8Ozj8CARCAO7opMolPJUmBXd39Zlp0L2H9fzMKidHm1vvaF6nNFq0ClRY7FlIZmTm4JfnOebPseffiXFn9tG8cq7oi
enc_ts: 1439568549.245995
arn: arn:aws:kms:us-east-1:656532927350:key/920aff2e-c5f1-4040-943a-047fa387b27e

424
sops Executable file
Просмотреть файл

@ -0,0 +1,424 @@
#!/usr/bin/env python
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# Contributor: Julien Vehent jvehent@mozilla.com [:ulfr]
from __future__ import print_function
import os
import sys
import tempfile
import argparse
from base64 import b64encode, b64decode
from ruamel.yaml.comments import CommentedMap
from textwrap import dedent
import ruamel.yaml
import json
import boto3
from cryptography.hazmat.primitives.ciphers import Cipher, modes, algorithms
from cryptography.hazmat.backends import default_backend
import time
import subprocess
import random
import re
DESC = """
`sops` encrypts and decrypts secrets using AWS KMS. It requires access
to AWS uising credentials in ~/.aws/credentials .
The ARN of the KMS Key used to encrypt/decrypt a document must be specified
in a top-level key of the document. For example:
YAML
sops:
kms:
arn: arn:aws:kms:us-east-1:656532927350:key/305caadb
JSON
{"sops":{"kms":{"arn": "arn:aws:kms:us-east-1:650:key/305caadb"}}}
TEXT
SOPS_KMS_ARN="arn:aws:kms:us-east-1:6565350:key/30db-b886-4e12-8d"
Alternatively, the KMS Key ID can be defined on the command line (-k flag) and
in the environment variable $SOPS_KMS_ARN. The order of preference to select
which KMS ID to use is: 1) file, 2) -k flag, 3) environment variable.
By default, editing is done in vim. Set the env variable $EDITOR to use
a different editor.
Mozilla Services - ulfr, relud - 2015
"""
SOPS_KMS_ARN = ""
SOPS_FOOTER = "# --- sops encryption info. do not edit. ---"
def main():
argparser = argparse.ArgumentParser(
usage='sops <file>',
formatter_class=argparse.RawDescriptionHelpFormatter,
description='Encrypted secrets editor',
epilog=dedent(DESC))
argparser.add_argument('file',
help="file to edit; create it if it doesn't exist")
argparser.add_argument('-k', '--kms', dest='kmsarn',
help="ARN of KMS key used for (en|de)cryption, "
"if none is defined in <file>.")
argparser.add_argument('-d', '--decrypt', action='store_true',
dest='decrypt',
help="Decrypt <file> and print it to stdout")
argparser.add_argument('-e', '--encrypt', action='store_true',
dest='encrypt',
help="encrypt <file> and print it to stdout")
argparser.add_argument('--input-type', dest='input_type',
help="input type (yaml, json, ...). "
"If undef, use file extension.")
argparser.add_argument('--output-type', dest='output_type',
help="output type (yaml, json, ...). "
"If undef, use input type.")
args = argparser.parse_args()
global SOPS_KMS_ARN
if args.kmsarn:
SOPS_KMS_ARN = args.kmsarn
elif 'SOPS_KMS_ARN' in os.environ:
SOPS_KMS_ARN = os.environ['SOPS_KMS_ARN']
if args.input_type:
itype = args.input_type
else:
itype = detect_filetype(args.file)
if args.output_type:
otype = args.output_type
else:
otype = itype
try:
fstat = os.stat(args.file)
# read the encrypted file from disk
tree = load_tree(args.file, itype)
except:
if args.encrypt or args.decrypt:
panic("cannot operate on non-existent file")
print("%s doesn't exist, creating it." % args.file)
tree = dict()
if args.encrypt:
# Encrypt mode: encrypt, display and exit
key = get_kms_data_key(tree)
tree = walk_and_encrypt(tree, key)
elif args.decrypt:
# Decrypt mode: decrypt, display and exit
key = get_kms_data_key(tree)
tree = walk_and_decrypt(tree, key)
else:
# EDIT Mode: decrypt, edit, encrypt and save
key = get_kms_data_key(tree)
# we need a stash to save the IV and AAD and reuse them
# if a given value has not changed during editing
stash = {'sops': {'has_stash': True}}
tree = walk_and_decrypt(tree, key, stash=stash)
# the decrypted tree is written to a tempfile and an editor
# is opened on the file
tmppath = write_file(tree, filetype=otype)
tmpstamp = os.stat(tmppath)
run_editor(tmppath)
# verify if file has been modified, and if not, just exit
tmpstamp2 = os.stat(tmppath)
if tmpstamp == tmpstamp2:
os.remove(tmppath)
panic("%s has not been modified, exit without writing" % args.file)
# encrypt the tree
tree = load_tree(tmppath, otype)
os.remove(tmppath)
tree = walk_and_encrypt(tree, key, stash)
# if we're in -e or -d mode, display to stdout
if args.encrypt or args.decrypt:
tmppath = write_file(tree, filetype=otype)
with open(tmppath, 'r') as f:
print(f.read())
os.remove(tmppath)
# otherwise, write the encrypted tree to a file
else:
path = write_file(tree, path=args.file, filetype=otype)
print("file written to %s" % (path), file=sys.stderr)
def detect_filetype(file):
"""
Detect the type of file based on its extension.
Return a string that describes the format: `text`, `yaml`, `json`
"""
if len(file) > 5:
if file[-5:] == '.yaml':
return 'yaml'
elif file[-5:] == '.json':
return 'json'
return 'text'
def load_tree(path, filetype):
"""
Read data from `path` using format defined by `filetype`.
Return a dictionary with the data
"""
with open(path, "r") as fd:
if filetype == 'yaml':
return ruamel.yaml.load(fd, ruamel.yaml.RoundTripLoader)
elif filetype == 'json':
return json.load(fd)
else:
tree = dict()
tree['data'] = ""
tree['sops'] = dict()
tree['sops']['kms'] = dict()
for line in fd:
if line.startswith(SOPS_FOOTER):
continue
elif line.startswith('SOPS_KMS_ARN'):
tree['sops']['kms']['arn'] = line.rstrip('\n').split('=', 1)[1]
elif line.startswith('SOPS_KMS_ENCTS'):
tree['sops']['kms']['enc_ts'] = line.rstrip('\n').split('=', 1)[1]
elif line.startswith('SOPS_KMS_ENC'):
tree['sops']['kms']['enc'] = line.rstrip('\n').split('=', 1)[1]
else:
tree['data'] += line
return tree
def walk_and_decrypt(branch, key, stash=None):
"""
Walk the branch recursively and decrypt leaves
"""
for k, v in branch.items():
if k == 'sops':
continue # everything under the `sops` key stays in clear
nstash = dict()
if stash:
stash[k] = {'has_stash': True}
nstash = stash[k]
if isinstance(v, dict):
branch[k] = walk_and_decrypt(v, key, nstash)
else:
# this is a value, decrypt it
if isinstance(v, ruamel.yaml.scalarstring.PreservedScalarString):
ev = decrypt(str(v), key, nstash)
branch[k] = ruamel.yaml.scalarstring.PreservedScalarString(ev)
elif isinstance(v, list):
lstash = dict()
kl = []
for i, lv in enumerate(list(v)):
if nstash:
nstash[i] = {'has_stash': True}
lstash = nstash[i]
kl.append(decrypt(lv, key, lstash))
branch[k] = kl
else:
branch[k] = decrypt(v, key, nstash)
return branch
def decrypt(value, key, stash=None):
"""
Return a decrypted value
"""
# extract fields using a regex
res = re.match(r'^ENC\[AES256_GCM,data=(.+),iv=(.+),aad=(.+),tag=(.+)\]$',
value)
enc_value = b64decode(res.group(1))
iv = b64decode(res.group(2))
aad = b64decode(res.group(3))
tag = b64decode(res.group(4))
decryptor = Cipher(algorithms.AES(key),
modes.GCM(iv, tag),
default_backend()).decryptor()
decryptor.authenticate_additional_data(aad)
cleartext = decryptor.update(enc_value) + decryptor.finalize()
if stash:
# save the values for later if we need to reencrypt
stash['iv'] = iv
stash['aad'] = aad
stash['cleartext'] = cleartext
return cleartext
def walk_and_encrypt(branch, key, stash=None):
"""
Walk the branch recursively and call encrypt of leaves.
"""
for k, v in branch.items():
if k == 'sops':
continue # everything under the `sops` key stays in clear
nstash = dict()
if stash and k in stash:
nstash = stash[k]
if isinstance(v, dict):
# recursively walk the tree
branch[k] = walk_and_encrypt(v, key, nstash)
else:
# this is a value, convert v to an encryptable type
# and encrypt
if isinstance(v, ruamel.yaml.scalarstring.PreservedScalarString):
ev = encrypt(str(v), key, nstash)
branch[k] = ruamel.yaml.scalarstring.PreservedScalarString(ev)
elif type(v) is not list and isinstance(v, list):
lstash = dict()
kl = []
for i, lv in enumerate(list(v)):
if nstash and i in nstash:
lstash = nstash[i]
kl.append(encrypt(lv, key, lstash))
branch[k] = kl
else:
branch[k] = encrypt(v, key, nstash)
return branch
def encrypt(value, key, stash=None):
"""
Return an encrypted string of the value provided.
"""
# if we have a stash, and the value of cleartext has not changed,
# attempt to take the IV and AAD value from the stash.
# if the stash has no existing value, or the cleartext has changed,
# generate new IV and AAD.
if stash and stash['cleartext'] == value:
iv = stash['iv']
aad = stash['aad']
else:
iv = os.urandom(32)
aad = os.urandom(32)
encryptor = Cipher(algorithms.AES(key),
modes.GCM(iv),
default_backend()).encryptor()
encryptor.authenticate_additional_data(aad)
enc_value = encryptor.update(value) + encryptor.finalize()
return "ENC[AES256_GCM,data={value},iv={iv},aad={aad}," \
"tag={tag}]".format(value=b64encode(enc_value),
iv=b64encode(iv),
aad=b64encode(aad),
tag=b64encode(encryptor.tag))
def get_kms_arn(tree):
"""
Return the ARN of the KMS Key used to encrypt/decrypt the AES key.
If the value exists in the tree, use it. Otherwise try to read it
from env variable SOPS_KMS_ARN. If neither exist, panic!
"""
if 'sops' not in tree or \
'kms' not in tree['sops'] or \
'arn' not in tree['sops']['kms'] or \
tree['sops']['kms']['arn'] == "":
# key is not in tree, try the env variable and store it in the tree
if SOPS_KMS_ARN != "":
if 'sops' not in tree:
tree['sops'] = dict()
if 'kms' not in tree['sops']:
tree['sops']['kms'] = dict()
tree['sops']['kms']['arn'] = SOPS_KMS_ARN
return SOPS_KMS_ARN
else:
panic("KMS ARN not found, unable to continue")
return tree['sops']['kms']['arn']
def get_kms_data_key(tree):
"""
Obtain a key and an IV. The values are either stored in encrypted form in
the tree, and we ask KMS to decrypt it, or it doesn't exist and we
ask KMS to generate one.
Return a 32 bytes key
"""
kms = boto3.client('kms')
kms_key = get_kms_arn(tree)
if 'sops' not in tree or \
'kms' not in tree['sops'] or \
'enc' not in tree['sops']['kms'] or \
tree['sops']['kms'] == "":
# no key found, ask KMS for a data key
try:
kms_response = kms.generate_data_key(KeyId=kms_key,
NumberOfBytes=32)
except:
panic("Could not obtain data key from KMS using ARN %s" % kms_key)
# store the encrypted blob of the data key in the tree
if 'sops' not in tree:
tree['sops'] = dict()
if 'kms' not in tree['sops']:
tree['sops']['kms'] = dict()
tree['sops']['kms']['enc'] = b64encode(kms_response['CiphertextBlob'])
tree['sops']['kms']['enc_ts'] = time.time()
print("new data key generated from kms: %s..." %
tree['sops']['kms']['enc'][0:32], file=sys.stderr)
else:
# use existing data key, ask kms to decrypt it
try:
kms_response = kms.decrypt(
CiphertextBlob=b64decode(tree['sops']['kms']['enc']))
except Exception as e:
panic("Data key decryption error %s" % e)
return kms_response['Plaintext'][:32]
def write_file(tree, path=None, filetype=None):
"""
Write the content of `tree` encoded using the format defined by `filetype`
at the location `path`.
If `path` is not defined, a tempfile is created.
if `filetype` is not defined, tree is treated as a blob of data.
Return the path of the file written.
"""
if path:
fd = open(path, "wb")
else:
fd = tempfile.NamedTemporaryFile(suffix="."+filetype, delete=False)
path = fd.name
if filetype == "yaml":
fd.write(ruamel.yaml.dump(tree, Dumper=ruamel.yaml.RoundTripDumper,
indent=4))
elif filetype == "json":
json.dump(tree, fd, sort_keys=True, indent=4)
else:
if 'data' in tree:
fd.write(tree['data'] + "\n")
if 'sops' in tree and 'kms' in tree['sops']:
fd.write("%s\n" % SOPS_FOOTER)
if 'arn' in tree['sops']['kms']:
fd.write("SOPS_KMS_ARN=%s\n" % tree['sops']['kms']['arn'])
if 'enc' in tree['sops']['kms']:
fd.write("SOPS_KMS_ENC=%s\n" % tree['sops']['kms']['enc'])
if 'enc_ts' in tree['sops']['kms']:
fd.write("SOPS_KMS_ENCTS=%s\n" %
tree['sops']['kms']['enc_ts'])
fd.close()
return path
def run_editor(path):
"""
Call a text editor on the file given by path.
"""
editor = "vim"
if 'EDITOR' in os.environ:
editor = os.environ['EDITOR']
subprocess.call([editor, path])
return
def panic(msg):
from sys import exit
print(msg, file=sys.stderr)
exit(1)
if __name__ == '__main__':
main()