You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508
  1. --[[
  2. Copyright (c) 2022, Vsevolod Stakhov <vsevolod@rspamd.com>
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. ]]--
  13. local argparse = require "argparse"
  14. local rspamd_keypair = require "rspamd_cryptobox_keypair"
  15. local rspamd_pubkey = require "rspamd_cryptobox_pubkey"
  16. local rspamd_signature = require "rspamd_cryptobox_signature"
  17. local rspamd_crypto = require "rspamd_cryptobox"
  18. local rspamd_util = require "rspamd_util"
  19. local ucl = require "ucl"
  20. local logger = require "rspamd_logger"
  21. -- Define command line options
  22. local parser = argparse()
  23. :name "rspamadm keypair"
  24. :description "Manages keypairs for Rspamd"
  25. :help_description_margin(30)
  26. :command_target("command")
  27. :require_command(false)
  28. -- Generate subcommand
  29. local generate = parser:command "generate gen g"
  30. :description "Creates a new keypair"
  31. generate:flag "-s --sign"
  32. :description "Generates a sign keypair instead of the encryption one"
  33. generate:flag "-n --nist"
  34. :description "Uses nist encryption algorithm"
  35. generate:option "-o --output"
  36. :description "Write keypair to file"
  37. :argname "<file>"
  38. generate:mutex(
  39. generate:flag "-j --json"
  40. :description "Output JSON instead of UCL",
  41. generate:flag "-u --ucl"
  42. :description "Output UCL"
  43. :default(true)
  44. )
  45. generate:option "--name"
  46. :description "Adds name extension"
  47. :argname "<name>"
  48. -- Sign subcommand
  49. local sign = parser:command "sign sig s"
  50. :description "Signs a file using keypair"
  51. sign:option "-k --keypair"
  52. :description "Keypair to use"
  53. :argname "<file>"
  54. sign:option "-s --suffix"
  55. :description "Suffix for signature"
  56. :argname "<suffix>"
  57. :default("sig")
  58. sign:argument "file"
  59. :description "File to sign"
  60. :argname "<file>"
  61. :args "*"
  62. -- Verify subcommand
  63. local verify = parser:command "verify ver v"
  64. :description "Verifies a file using keypair or a public key"
  65. verify:mutex(
  66. verify:option "-p --pubkey"
  67. :description "Load pubkey from the specified file"
  68. :argname "<file>",
  69. verify:option "-P --pubstring"
  70. :description "Load pubkey from the base32 encoded string"
  71. :argname "<base32>",
  72. verify:option "-k --keypair"
  73. :description "Get pubkey from the keypair file"
  74. :argname "<file>"
  75. )
  76. verify:argument "file"
  77. :description "File to verify"
  78. :argname "<file>"
  79. :args "*"
  80. verify:flag "-n --nist"
  81. :description "Uses nistp curves (P256)"
  82. verify:option "-s --suffix"
  83. :description "Suffix for signature"
  84. :argname "<suffix>"
  85. :default("sig")
  86. -- Encrypt subcommand
  87. local encrypt = parser:command "encrypt crypt enc e"
  88. :description "Encrypts a file using keypair (or a pubkey)"
  89. encrypt:mutex(
  90. encrypt:option "-p --pubkey"
  91. :description "Load pubkey from the specified file"
  92. :argname "<file>",
  93. encrypt:option "-P --pubstring"
  94. :description "Load pubkey from the base32 encoded string"
  95. :argname "<base32>",
  96. encrypt:option "-k --keypair"
  97. :description "Get pubkey from the keypair file"
  98. :argname "<file>"
  99. )
  100. encrypt:option "-s --suffix"
  101. :description "Suffix for encrypted file"
  102. :argname "<suffix>"
  103. :default("enc")
  104. encrypt:argument "file"
  105. :description "File to encrypt"
  106. :argname "<file>"
  107. :args "*"
  108. encrypt:flag "-r --rm"
  109. :description "Remove unencrypted file"
  110. encrypt:flag "-f --force"
  111. :description "Remove destination file if it exists"
  112. -- Decrypt subcommand
  113. local decrypt = parser:command "decrypt dec d"
  114. :description "Decrypts a file using keypair"
  115. decrypt:option "-k --keypair"
  116. :description "Get pubkey from the keypair file"
  117. :argname "<file>"
  118. decrypt:flag "-S --keep-suffix"
  119. :description "Preserve suffix for decrypted file (overwrite encrypted)"
  120. decrypt:argument "file"
  121. :description "File to encrypt"
  122. :argname "<file>"
  123. :args "*"
  124. decrypt:flag "-f --force"
  125. :description "Remove destination file if it exists (implied with -S)"
  126. decrypt:flag "-r --rm"
  127. :description "Remove encrypted file"
  128. -- Default command is generate, so duplicate options to be compatible
  129. parser:flag "-s --sign"
  130. :description "Generates a sign keypair instead of the encryption one"
  131. parser:flag "-n --nist"
  132. :description "Uses nistp curves (P256)"
  133. parser:mutex(
  134. parser:flag "-j --json"
  135. :description "Output JSON instead of UCL",
  136. parser:flag "-u --ucl"
  137. :description "Output UCL"
  138. :default(true)
  139. )
  140. parser:option "-o --output"
  141. :description "Write keypair to file"
  142. :argname "<file>"
  143. local function fatal(...)
  144. logger.errx(...)
  145. os.exit(1)
  146. end
  147. local function ask_yes_no(greet, default)
  148. local def_str
  149. if default then
  150. greet = greet .. "[Y/n]: "
  151. def_str = "yes"
  152. else
  153. greet = greet .. "[y/N]: "
  154. def_str = "no"
  155. end
  156. local reply = rspamd_util.readline(greet)
  157. if not reply then
  158. os.exit(0)
  159. end
  160. if #reply == 0 then
  161. reply = def_str
  162. end
  163. reply = reply:lower()
  164. if reply == 'y' or reply == 'yes' then
  165. return true
  166. end
  167. return false
  168. end
  169. local function generate_handler(opts)
  170. local mode = 'encryption'
  171. if opts.sign then
  172. mode = 'sign'
  173. end
  174. local alg = 'curve25519'
  175. if opts.nist then
  176. alg = 'nist'
  177. end
  178. -- TODO: probably, do it in a more safe way
  179. local kp = rspamd_keypair.create(mode, alg):totable()
  180. if opts.name then
  181. kp.keypair.extensions = {
  182. name = opts.name
  183. }
  184. end
  185. local format = 'ucl'
  186. if opts.json then
  187. format = 'json'
  188. end
  189. if opts.output then
  190. local out = io.open(opts.output, 'w')
  191. if not out then
  192. fatal('cannot open output to write: ' .. opts.output)
  193. end
  194. out:write(ucl.to_format(kp, format))
  195. out:close()
  196. else
  197. io.write(ucl.to_format(kp, format))
  198. end
  199. end
  200. local function sign_handler(opts)
  201. if opts.file then
  202. if type(opts.file) == 'string' then
  203. opts.file = { opts.file }
  204. end
  205. else
  206. parser:error('no files to sign')
  207. end
  208. if not opts.keypair then
  209. parser:error("no keypair specified")
  210. end
  211. local ucl_parser = ucl.parser()
  212. local res, err = ucl_parser:parse_file(opts.keypair)
  213. if not res then
  214. fatal(string.format('cannot load %s: %s', opts.keypair, err))
  215. end
  216. local kp = rspamd_keypair.load(ucl_parser:get_object())
  217. if not kp then
  218. fatal("cannot load keypair: " .. opts.keypair)
  219. end
  220. for _, fname in ipairs(opts.file) do
  221. local sig = rspamd_crypto.sign_file(kp, fname)
  222. if not sig then
  223. fatal(string.format("cannot sign %s\n", fname))
  224. end
  225. local out = string.format('%s.%s', fname, opts.suffix or 'sig')
  226. local of = io.open(out, 'w')
  227. if not of then
  228. fatal('cannot open output to write: ' .. out)
  229. end
  230. of:write(sig:bin())
  231. of:close()
  232. io.write(string.format('signed %s -> %s (%s)\n', fname, out, sig:hex()))
  233. end
  234. end
  235. local function verify_handler(opts)
  236. if opts.file then
  237. if type(opts.file) == 'string' then
  238. opts.file = { opts.file }
  239. end
  240. else
  241. parser:error('no files to verify')
  242. end
  243. local pk
  244. local alg = 'curve25519'
  245. if opts.keypair then
  246. local ucl_parser = ucl.parser()
  247. local res, err = ucl_parser:parse_file(opts.keypair)
  248. if not res then
  249. fatal(string.format('cannot load %s: %s', opts.keypair, err))
  250. end
  251. local kp = rspamd_keypair.load(ucl_parser:get_object())
  252. if not kp then
  253. fatal("cannot load keypair: " .. opts.keypair)
  254. end
  255. pk = kp:pk()
  256. alg = kp:alg()
  257. elseif opts.pubkey then
  258. if opts.nist then
  259. alg = 'nist'
  260. end
  261. pk = rspamd_pubkey.load(opts.pubkey, 'sign', alg)
  262. elseif opts.pubstr then
  263. if opts.nist then
  264. alg = 'nist'
  265. end
  266. pk = rspamd_pubkey.create(opts.pubstr, 'sign', alg)
  267. end
  268. if not pk then
  269. fatal("cannot create pubkey")
  270. end
  271. local valid = true
  272. for _, fname in ipairs(opts.file) do
  273. local sig_fname = string.format('%s.%s', fname, opts.suffix or 'sig')
  274. local sig = rspamd_signature.load(sig_fname, alg)
  275. if not sig then
  276. fatal(string.format("cannot load signature for %s -> %s",
  277. fname, sig_fname))
  278. end
  279. if rspamd_crypto.verify_file(pk, sig, fname, alg) then
  280. io.write(string.format('verified %s -> %s (%s)\n', fname, sig_fname, sig:hex()))
  281. else
  282. valid = false
  283. io.write(string.format('FAILED to verify %s -> %s (%s)\n', fname,
  284. sig_fname, sig:hex()))
  285. end
  286. end
  287. if not valid then
  288. os.exit(1)
  289. end
  290. end
  291. local function encrypt_handler(opts)
  292. if opts.file then
  293. if type(opts.file) == 'string' then
  294. opts.file = { opts.file }
  295. end
  296. else
  297. parser:error('no files to sign')
  298. end
  299. local pk
  300. local alg = 'curve25519'
  301. if opts.keypair then
  302. local ucl_parser = ucl.parser()
  303. local res, err = ucl_parser:parse_file(opts.keypair)
  304. if not res then
  305. fatal(string.format('cannot load %s: %s', opts.keypair, err))
  306. end
  307. local kp = rspamd_keypair.load(ucl_parser:get_object())
  308. if not kp then
  309. fatal("cannot load keypair: " .. opts.keypair)
  310. end
  311. pk = kp:pk()
  312. alg = kp:alg()
  313. elseif opts.pubkey then
  314. if opts.nist then
  315. alg = 'nist'
  316. end
  317. pk = rspamd_pubkey.load(opts.pubkey, 'sign', alg)
  318. elseif opts.pubstr then
  319. if opts.nist then
  320. alg = 'nist'
  321. end
  322. pk = rspamd_pubkey.create(opts.pubstr, 'sign', alg)
  323. end
  324. if not pk then
  325. fatal("cannot load keypair: " .. opts.keypair)
  326. end
  327. for _, fname in ipairs(opts.file) do
  328. local enc = rspamd_crypto.encrypt_file(pk, fname, alg)
  329. if not enc then
  330. fatal(string.format("cannot encrypt %s\n", fname))
  331. end
  332. local out
  333. if opts.suffix and #opts.suffix > 0 then
  334. out = string.format('%s.%s', fname, opts.suffix)
  335. else
  336. out = string.format('%s', fname)
  337. end
  338. if rspamd_util.file_exists(out) then
  339. if opts.force or ask_yes_no(string.format('File %s already exists, overwrite?',
  340. out), true) then
  341. os.remove(out)
  342. else
  343. os.exit(1)
  344. end
  345. end
  346. enc:save_in_file(out)
  347. if opts.rm then
  348. os.remove(fname)
  349. io.write(string.format('encrypted %s (deleted) -> %s\n', fname, out))
  350. else
  351. io.write(string.format('encrypted %s -> %s\n', fname, out))
  352. end
  353. end
  354. end
  355. local function decrypt_handler(opts)
  356. if opts.file then
  357. if type(opts.file) == 'string' then
  358. opts.file = { opts.file }
  359. end
  360. else
  361. parser:error('no files to decrypt')
  362. end
  363. if not opts.keypair then
  364. parser:error("no keypair specified")
  365. end
  366. local ucl_parser = ucl.parser()
  367. local res, err = ucl_parser:parse_file(opts.keypair)
  368. if not res then
  369. fatal(string.format('cannot load %s: %s', opts.keypair, err))
  370. end
  371. local kp = rspamd_keypair.load(ucl_parser:get_object())
  372. if not kp then
  373. fatal("cannot load keypair: " .. opts.keypair)
  374. end
  375. for _, fname in ipairs(opts.file) do
  376. local decrypted = rspamd_crypto.decrypt_file(kp, fname)
  377. if not decrypted then
  378. fatal(string.format("cannot decrypt %s\n", fname))
  379. end
  380. local out
  381. if not opts['keep-suffix'] then
  382. -- Strip the last suffix
  383. out = fname:match("^(.+)%..+$")
  384. else
  385. out = fname
  386. end
  387. local removed = false
  388. if rspamd_util.file_exists(out) then
  389. if (opts.force or opts['keep-suffix'])
  390. or ask_yes_no(string.format('File %s already exists, overwrite?', out), true) then
  391. os.remove(out)
  392. removed = true
  393. else
  394. os.exit(1)
  395. end
  396. end
  397. if opts.rm then
  398. os.remove(fname)
  399. removed = true
  400. end
  401. if removed then
  402. io.write(string.format('decrypted %s (removed) -> %s\n', fname, out))
  403. else
  404. io.write(string.format('decrypted %s -> %s\n', fname, out))
  405. end
  406. end
  407. end
  408. local function handler(args)
  409. local opts = parser:parse(args)
  410. local command = opts.command or "generate"
  411. if command == 'generate' then
  412. generate_handler(opts)
  413. elseif command == 'sign' then
  414. sign_handler(opts)
  415. elseif command == 'verify' then
  416. verify_handler(opts)
  417. elseif command == 'encrypt' then
  418. encrypt_handler(opts)
  419. elseif command == 'decrypt' then
  420. decrypt_handler(opts)
  421. else
  422. parser:error('command %s is not implemented', command)
  423. end
  424. end
  425. return {
  426. name = 'keypair',
  427. aliases = { 'kp', 'key' },
  428. handler = handler,
  429. description = parser._description
  430. }