123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361 |
- #!/usr/bin/env node
- /*
- secrets management program
- Secrets are stored encrypted on S3 in a bucket called ${profile}.config.
- The secrets are defined in gitlab/keys and are copied to s3 via "MakeMe"
- */
- const USAGE = 'Usage: secrets --aws-profile profile [--env prefix] [--key file] [--key-string key] [--profile profile] [set filename| get fields...]'
- require('module').Module._initPaths()
- var Spawn = require('child_process')
- var Fs = require('fs')
- var Crypto = require('crypto')
- const IV_LENGTH = 16
- const CIPHER = 'aes-256-gcm'
- const SECRETS_FILE = 'secrets.json'
- const SALT = 'dead-sea:'
- const RECURSE_LIMIT = 75
- var awsProfile = null
- var env = null
- var json = false
- var command
- var fields
- var key = "/etc/farm/secret.key"
- var keyString
- var profile
- global.dump = (...args) => { for (let item of args) print(JSON.stringify(item, null, 4)) }
- global.print = (...args) => console.log(...args)
- function usage() {
- process.stderr.write(USAGE + '\n')
- process.exit(1)
- }
- function parseArgs() {
- let argv = process.argv.slice(2)
- for (let i = 0; i < argv.length; i++) {
- let arg = argv[i]
- if (arg == '--env') {
- env = argv[++i]
- } else if (arg == '-a' || arg == '--aws-profile') {
- awsProfile = argv[++i]
- } else if (arg == '-j' || arg == '--json') {
- json = true
- } else if (arg == '-k' || arg == '--key') {
- key = argv[++i]
- } else if (arg == '--key-string') {
- keyString = argv[++i]
- } else if (arg == '-p' || arg == '--profile') {
- profile = argv[++i]
- } else {
- argv = argv.slice(i)
- break
- }
- }
- command = argv[0]
- if (!command || !awsProfile) {
- usage()
- }
- if (command == 'get') {
- fields = argv.slice(1)
- } else if (command == 'set') {
- filename = argv[1]
- if (!filename) {
- usage()
- }
- }
- }
- function decrypt(text, password, inCode = 'base64', outCode = 'utf8') {
- if (text) {
- let [cipher, tag, iv, data] = text.split(':')
- iv = Buffer.from(iv, inCode)
- let secret = Crypto.createHash('sha256').update(SALT + password, 'utf8').digest()
- let crypt = Crypto.createDecipheriv(CIPHER, secret, iv)
- if (tag) {
- tag = Buffer.from(tag, inCode)
- crypt.setAuthTag(tag)
- }
- text = crypt.update(data.toString(), inCode, outCode) + crypt.final(outCode)
- }
- return JSON.parse(text.toString())
- }
- function encrypt(text, password, inCode = 'utf8', outCode = 'base64') {
- if (text) {
- let iv = Crypto.randomBytes(IV_LENGTH)
- let secret = Crypto.createHash('sha256').update(SALT + password, 'utf8').digest()
- let crypt = Crypto.createCipheriv(CIPHER, secret, iv)
- let crypted = crypt.update(text, inCode, outCode) + crypt.final(outCode)
- let tag = (CIPHER.indexOf('-gcm') > 0) ? crypt.getAuthTag().toString(outCode) : ''
- text = `${CIPHER}:${tag}:${iv.toString(outCode)}:${crypted}`
- }
- return text
- }
- function getSecrets() {
- let args = ['s3', 'cp', `s3://${awsProfile}.config/${awsProfile}/${SECRETS_FILE}`, '-', '--profile', awsProfile]
- let cmd = Spawn.spawnSync('aws', args)
- if (cmd.status != 0) {
- process.stderr.write(`Command failure: aws ${args.join(' ')}\n`)
- process.stderr.write(cmd.stderr.toString() + '\n')
- process.exit(2)
- }
- let data = cmd.stdout.toString()
- let password = (keyString) ? keyString : Fs.readFileSync(key)
- data = decrypt(data, password)
- return data
- }
- async function setSecrets() {
- let password = (keyString) ? keyString : Fs.readFileSync(key)
- let data = Fs.readFileSync(filename)
- JSON.parse(data)
- data = encrypt(data, password)
- await new Promise((resolve, reject) => {
- let args = ['s3', 'cp', '-', `s3://${awsProfile}.config/${awsProfile}/${SECRETS_FILE}`, '--profile', awsProfile]
- let cmd = Spawn.spawn('aws', args)
- cmd.stdin.write(data)
- cmd.stdin.end()
- cmd.stderr.on('data', (data) => {
- process.stderr.write(`Command failure: aws ${args.join(' ')}\n`)
- process.stderr.write(data + '\n')
- })
- cmd.on('close', (status) => {
- if (status != 0) {
- reject('aws command failed')
- } else {
- resolve(true)
- }
- })
- })
- }
- function printEnv(obj, prefix = '', vars = {}) {
- try {
- for (let name of Object.keys(obj)) {
- let value = obj[name]
- if (name == 'profiles') {
- continue
- }
- if (typeof value == 'object') {
- printEnv(value, prefix + name.toUpperCase() + '_', vars)
- } else {
- name = (prefix + name).toUpperCase().replace(/\./g, '_').replace(/-/g, '_')
- // vars[name] = value
- print('export ' + name + '="' + value + '"')
- }
- }
- } catch (e) {
- process.stderr.write("CATCH" + e.toString() + '\n')
- process.exit(2)
- }
- }
- async function run() {
- if (command == 'get') {
- let data = getSecrets()
- if (profile) {
- data = blendProfile(data, profile)
- }
- data = selectFields(data, fields)
- if (json) {
- dump(data)
- } else if (env != null) {
- printEnv(data, env)
- } else {
- print(data)
- }
- } else if (command == 'set') {
- await setSecrets()
- } else {
- usage()
- }
- }
- function blendProfile(obj, fields) {
- if (obj.profiles[profile]) {
- obj = blend(obj, obj.profiles[profile])
- }
- return obj
- }
- function selectFields(obj, fields) {
- let result = {}
- if (!fields || fields.length == 0) {
- return obj
- }
- for (field of Object.values(fields)) {
- let set = obj
- for (let part of field.split('.')) {
- set = set[part]
- if (!set) {
- break
- }
- }
- let type = typeof set
- if (type == 'string' || type == 'boolean' || type == 'number' || set == null) {
- result = set
- break
- } else {
- result = Object.assign(result, set)
- }
- }
- return result
- }
- function cleanup() {
- }
- async function main() {
- parseArgs()
- await run()
- cleanup()
- }
- async function start() {
- try {
- await main()
- } catch (e) {
- process.stderr.write(e.toString() + '\n')
- process.exit(1)
- }
- }
- start()
- function blend(dest, src, combine = '', recurse = 0) {
- if (recurse > RECURSE_LIMIT) {
- return
- }
- if (!src) {
- return dest
- }
- if (!dest || typeof dest != 'object' || Array.isArray(dest)) {
- return dest
- }
- for (let key of Object.getOwnPropertyNames(src)) {
- let property = key
- let op = key[0]
- if (op == '+') {
- property = key.slice(1)
- } else if (op == '-') {
- property = key.slice(1)
- } else if (op == '?') {
- property = key.slice(1)
- } else if (op == '=') {
- property = key.slice(1)
- } else if (combine) {
- op = combine
- } else {
- /* Default is to blend objects and assign arrays */
- op = ''
- }
- let s = src[key]
- let d = dest[property]
- if (!dest.hasOwnProperty(property)) {
- if (op == '-') {
- continue
- }
- dest[property] = clone(s)
- continue
- } else if (op == '?' && d != null) {
- continue
- }
- if (Array.isArray(d)) {
- if (op == '=') {
- /* op == '=' */
- dest[property] = clone(s)
- } else if (op == '-') {
- if (Array.isArray(s)) {
- for (let item of s) {
- let index = d.indexOf(item)
- if (index >= 0) d.slice(index, 1)
- }
- } else {
- let index = d.indexOf(s)
- if (index >= 0) d.slice(index, 1)
- }
- } else if (op == '+') {
- /*
- This was the default, but blending Package.sensors.http.path from PackageOverride needs to
- overwrite and not union.
- */
- if (Array.isArray(s)) {
- for (let item of s) {
- if (d.indexOf(s) < 0) d.push(item)
- }
- } else {
- d.push(s)
- }
- } else {
- dest[property] = clone(s)
- }
- } else if (d instanceof Date) {
- if (op == '+') {
- dest[property] += s
- } else if (op == '-') {
- dest[property] -= s
- } else {
- /* op == '=' */
- dest[property] = s
- }
- } else if (typeof d == 'object' && d !== null && d !== undefined) {
- if (op == '=') {
- dest[property] = clone(s)
- } else if (op == '-') {
- delete dest[property]
- } else if (s === null) {
- dest[property] = s
- } else if (typeof s == 'object') {
- blend(d, s, op, recurse + 1)
- } else {
- dest[property] = s
- }
- } else if (typeof d == 'string') {
- if (op == '+') {
- dest[property] += ' ' + s
- } else if (op == '-') {
- if (d == s) {
- delete dest[property]
- } else {
- dest[property] = d.replace(s, '')
- }
- } else {
- /* op == '=' */
- dest[property] = s
- }
- } else if (typeof d == 'number') {
- if (op == '+') {
- dest[property] += s
- } else if (op == '-') {
- dest[property] -= s
- } else {
- /* op == '=' */
- dest[property] = s
- }
- } else {
- if (op == '=') {
- dest[property] = s
- } else if (op == '-') {
- delete dest[property]
- } else {
- dest[property] = s
- }
- }
- }
- return dest
- }
|