secrets 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. #!/usr/bin/env node
  2. /*
  3. secrets management program
  4. Secrets are stored encrypted on S3 in a bucket called ${profile}.config.
  5. The secrets are defined in gitlab/keys and are copied to s3 via "MakeMe"
  6. */
  7. const USAGE = 'Usage: secrets --aws-profile profile [--env prefix] [--key file] [--key-string key] [--profile profile] [set filename| get fields...]'
  8. require('module').Module._initPaths()
  9. var Spawn = require('child_process')
  10. var Fs = require('fs')
  11. var Crypto = require('crypto')
  12. const IV_LENGTH = 16
  13. const CIPHER = 'aes-256-gcm'
  14. const SECRETS_FILE = 'secrets.json'
  15. const SALT = 'dead-sea:'
  16. const RECURSE_LIMIT = 75
  17. var awsProfile = null
  18. var env = null
  19. var json = false
  20. var command
  21. var fields
  22. var key = "/etc/farm/secret.key"
  23. var keyString
  24. var profile
  25. global.dump = (...args) => { for (let item of args) print(JSON.stringify(item, null, 4)) }
  26. global.print = (...args) => console.log(...args)
  27. function usage() {
  28. process.stderr.write(USAGE + '\n')
  29. process.exit(1)
  30. }
  31. function parseArgs() {
  32. let argv = process.argv.slice(2)
  33. for (let i = 0; i < argv.length; i++) {
  34. let arg = argv[i]
  35. if (arg == '--env') {
  36. env = argv[++i]
  37. } else if (arg == '-a' || arg == '--aws-profile') {
  38. awsProfile = argv[++i]
  39. } else if (arg == '-j' || arg == '--json') {
  40. json = true
  41. } else if (arg == '-k' || arg == '--key') {
  42. key = argv[++i]
  43. } else if (arg == '--key-string') {
  44. keyString = argv[++i]
  45. } else if (arg == '-p' || arg == '--profile') {
  46. profile = argv[++i]
  47. } else {
  48. argv = argv.slice(i)
  49. break
  50. }
  51. }
  52. command = argv[0]
  53. if (!command || !awsProfile) {
  54. usage()
  55. }
  56. if (command == 'get') {
  57. fields = argv.slice(1)
  58. } else if (command == 'set') {
  59. filename = argv[1]
  60. if (!filename) {
  61. usage()
  62. }
  63. }
  64. }
  65. function decrypt(text, password, inCode = 'base64', outCode = 'utf8') {
  66. if (text) {
  67. let [cipher, tag, iv, data] = text.split(':')
  68. iv = Buffer.from(iv, inCode)
  69. let secret = Crypto.createHash('sha256').update(SALT + password, 'utf8').digest()
  70. let crypt = Crypto.createDecipheriv(CIPHER, secret, iv)
  71. if (tag) {
  72. tag = Buffer.from(tag, inCode)
  73. crypt.setAuthTag(tag)
  74. }
  75. text = crypt.update(data.toString(), inCode, outCode) + crypt.final(outCode)
  76. }
  77. return JSON.parse(text.toString())
  78. }
  79. function encrypt(text, password, inCode = 'utf8', outCode = 'base64') {
  80. if (text) {
  81. let iv = Crypto.randomBytes(IV_LENGTH)
  82. let secret = Crypto.createHash('sha256').update(SALT + password, 'utf8').digest()
  83. let crypt = Crypto.createCipheriv(CIPHER, secret, iv)
  84. let crypted = crypt.update(text, inCode, outCode) + crypt.final(outCode)
  85. let tag = (CIPHER.indexOf('-gcm') > 0) ? crypt.getAuthTag().toString(outCode) : ''
  86. text = `${CIPHER}:${tag}:${iv.toString(outCode)}:${crypted}`
  87. }
  88. return text
  89. }
  90. function getSecrets() {
  91. let args = ['s3', 'cp', `s3://${awsProfile}.config/${awsProfile}/${SECRETS_FILE}`, '-', '--profile', awsProfile]
  92. let cmd = Spawn.spawnSync('aws', args)
  93. if (cmd.status != 0) {
  94. process.stderr.write(`Command failure: aws ${args.join(' ')}\n`)
  95. process.stderr.write(cmd.stderr.toString() + '\n')
  96. process.exit(2)
  97. }
  98. let data = cmd.stdout.toString()
  99. let password = (keyString) ? keyString : Fs.readFileSync(key)
  100. data = decrypt(data, password)
  101. return data
  102. }
  103. async function setSecrets() {
  104. let password = (keyString) ? keyString : Fs.readFileSync(key)
  105. let data = Fs.readFileSync(filename)
  106. JSON.parse(data)
  107. data = encrypt(data, password)
  108. await new Promise((resolve, reject) => {
  109. let args = ['s3', 'cp', '-', `s3://${awsProfile}.config/${awsProfile}/${SECRETS_FILE}`, '--profile', awsProfile]
  110. let cmd = Spawn.spawn('aws', args)
  111. cmd.stdin.write(data)
  112. cmd.stdin.end()
  113. cmd.stderr.on('data', (data) => {
  114. process.stderr.write(`Command failure: aws ${args.join(' ')}\n`)
  115. process.stderr.write(data + '\n')
  116. })
  117. cmd.on('close', (status) => {
  118. if (status != 0) {
  119. reject('aws command failed')
  120. } else {
  121. resolve(true)
  122. }
  123. })
  124. })
  125. }
  126. function printEnv(obj, prefix = '', vars = {}) {
  127. try {
  128. for (let name of Object.keys(obj)) {
  129. let value = obj[name]
  130. if (name == 'profiles') {
  131. continue
  132. }
  133. if (typeof value == 'object') {
  134. printEnv(value, prefix + name.toUpperCase() + '_', vars)
  135. } else {
  136. name = (prefix + name).toUpperCase().replace(/\./g, '_').replace(/-/g, '_')
  137. // vars[name] = value
  138. print('export ' + name + '="' + value + '"')
  139. }
  140. }
  141. } catch (e) {
  142. process.stderr.write("CATCH" + e.toString() + '\n')
  143. process.exit(2)
  144. }
  145. }
  146. async function run() {
  147. if (command == 'get') {
  148. let data = getSecrets()
  149. if (profile) {
  150. data = blendProfile(data, profile)
  151. }
  152. data = selectFields(data, fields)
  153. if (json) {
  154. dump(data)
  155. } else if (env != null) {
  156. printEnv(data, env)
  157. } else {
  158. print(data)
  159. }
  160. } else if (command == 'set') {
  161. await setSecrets()
  162. } else {
  163. usage()
  164. }
  165. }
  166. function blendProfile(obj, fields) {
  167. if (obj.profiles[profile]) {
  168. obj = blend(obj, obj.profiles[profile])
  169. }
  170. return obj
  171. }
  172. function selectFields(obj, fields) {
  173. let result = {}
  174. if (!fields || fields.length == 0) {
  175. return obj
  176. }
  177. for (field of Object.values(fields)) {
  178. let set = obj
  179. for (let part of field.split('.')) {
  180. set = set[part]
  181. if (!set) {
  182. break
  183. }
  184. }
  185. let type = typeof set
  186. if (type == 'string' || type == 'boolean' || type == 'number' || set == null) {
  187. result = set
  188. break
  189. } else {
  190. result = Object.assign(result, set)
  191. }
  192. }
  193. return result
  194. }
  195. function cleanup() {
  196. }
  197. async function main() {
  198. parseArgs()
  199. await run()
  200. cleanup()
  201. }
  202. async function start() {
  203. try {
  204. await main()
  205. } catch (e) {
  206. process.stderr.write(e.toString() + '\n')
  207. process.exit(1)
  208. }
  209. }
  210. start()
  211. function blend(dest, src, combine = '', recurse = 0) {
  212. if (recurse > RECURSE_LIMIT) {
  213. return
  214. }
  215. if (!src) {
  216. return dest
  217. }
  218. if (!dest || typeof dest != 'object' || Array.isArray(dest)) {
  219. return dest
  220. }
  221. for (let key of Object.getOwnPropertyNames(src)) {
  222. let property = key
  223. let op = key[0]
  224. if (op == '+') {
  225. property = key.slice(1)
  226. } else if (op == '-') {
  227. property = key.slice(1)
  228. } else if (op == '?') {
  229. property = key.slice(1)
  230. } else if (op == '=') {
  231. property = key.slice(1)
  232. } else if (combine) {
  233. op = combine
  234. } else {
  235. /* Default is to blend objects and assign arrays */
  236. op = ''
  237. }
  238. let s = src[key]
  239. let d = dest[property]
  240. if (!dest.hasOwnProperty(property)) {
  241. if (op == '-') {
  242. continue
  243. }
  244. dest[property] = clone(s)
  245. continue
  246. } else if (op == '?' && d != null) {
  247. continue
  248. }
  249. if (Array.isArray(d)) {
  250. if (op == '=') {
  251. /* op == '=' */
  252. dest[property] = clone(s)
  253. } else if (op == '-') {
  254. if (Array.isArray(s)) {
  255. for (let item of s) {
  256. let index = d.indexOf(item)
  257. if (index >= 0) d.slice(index, 1)
  258. }
  259. } else {
  260. let index = d.indexOf(s)
  261. if (index >= 0) d.slice(index, 1)
  262. }
  263. } else if (op == '+') {
  264. /*
  265. This was the default, but blending Package.sensors.http.path from PackageOverride needs to
  266. overwrite and not union.
  267. */
  268. if (Array.isArray(s)) {
  269. for (let item of s) {
  270. if (d.indexOf(s) < 0) d.push(item)
  271. }
  272. } else {
  273. d.push(s)
  274. }
  275. } else {
  276. dest[property] = clone(s)
  277. }
  278. } else if (d instanceof Date) {
  279. if (op == '+') {
  280. dest[property] += s
  281. } else if (op == '-') {
  282. dest[property] -= s
  283. } else {
  284. /* op == '=' */
  285. dest[property] = s
  286. }
  287. } else if (typeof d == 'object' && d !== null && d !== undefined) {
  288. if (op == '=') {
  289. dest[property] = clone(s)
  290. } else if (op == '-') {
  291. delete dest[property]
  292. } else if (s === null) {
  293. dest[property] = s
  294. } else if (typeof s == 'object') {
  295. blend(d, s, op, recurse + 1)
  296. } else {
  297. dest[property] = s
  298. }
  299. } else if (typeof d == 'string') {
  300. if (op == '+') {
  301. dest[property] += ' ' + s
  302. } else if (op == '-') {
  303. if (d == s) {
  304. delete dest[property]
  305. } else {
  306. dest[property] = d.replace(s, '')
  307. }
  308. } else {
  309. /* op == '=' */
  310. dest[property] = s
  311. }
  312. } else if (typeof d == 'number') {
  313. if (op == '+') {
  314. dest[property] += s
  315. } else if (op == '-') {
  316. dest[property] -= s
  317. } else {
  318. /* op == '=' */
  319. dest[property] = s
  320. }
  321. } else {
  322. if (op == '=') {
  323. dest[property] = s
  324. } else if (op == '-') {
  325. delete dest[property]
  326. } else {
  327. dest[property] = s
  328. }
  329. }
  330. }
  331. return dest
  332. }