age-lf.sf 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. #!/usr/bin/ruby
  2. # Author: Trizen
  3. # Date: 02 February 2022
  4. # Edit: 17 February 2022
  5. # https://github.com/trizen
  6. # A large file encryption tool, inspired by Age, using Curve25519 and CBC+Serpent for encrypting data.
  7. # See also:
  8. # https://github.com/FiloSottile/age
  9. # https://metacpan.org/pod/Crypt::CBC
  10. # https://metacpan.org/pod/Crypt::PK::X25519
  11. # This is a simplified version of `plage`, optimized for large files:
  12. # https://github.com/trizen/perl-scripts/blob/master/Encryption/plage.pl
  13. require('Crypt::CBC')
  14. require('Crypt::PK::X25519')
  15. require('JSON::PP')
  16. STDIN.binmode(:raw)
  17. STDOUT.binmode(:raw)
  18. define {
  19. SHORT_APPNAME = "age-lf",
  20. BUFFER_SIZE = (1024 * 1024),
  21. EXPORT_KEY_BASE = 62,
  22. VERSION = '0.01',
  23. }
  24. var :CONFIG = (
  25. cipher => 'Serpent',
  26. chain_mode => 'CBC',
  27. )
  28. func create_cipher (
  29. pass,
  30. cipher = CONFIG{:cipher},
  31. chain_mode = CONFIG{:chain_mode}
  32. ) {
  33. %O<Crypt::CBC>.new(
  34. '-pass' => $pass,
  35. '-cipher' => "Cipher::#{cipher}",
  36. '-chain_mode' => chain_mode.lc,
  37. '-pbkdf' => 'pbkdf2',
  38. )
  39. }
  40. func x25519_from_public (hex_key) {
  41. %O<Crypt::PK::X25519>.new.import_key(
  42. Hash(
  43. curve => "x25519",
  44. pub => hex_key,
  45. )
  46. )
  47. }
  48. func x25519_from_private (hex_key) {
  49. %O<Crypt::PK::X25519>.new.import_key(
  50. Hash(
  51. curve => "x25519",
  52. priv => hex_key,
  53. )
  54. )
  55. }
  56. func x25519_random_key {
  57. while (1) {
  58. var key = %O<Crypt::PK::X25519>.new.generate_key
  59. var hash = key.key2hash
  60. next if hash{:pub}.starts_with('0')
  61. next if hash{:priv}.starts_with('0')
  62. next if hash{:pub}.ends_with('0')
  63. next if hash{:priv}.ends_with('0')
  64. return key
  65. }
  66. }
  67. func encrypt (fh, public_key) {
  68. # Generate a random ephemeral key-pair.
  69. var random_ephem_key = x25519_random_key()
  70. # Create a shared secret, using the random key and the reciever's public key
  71. var shared_secret = random_ephem_key.shared_secret(public_key)
  72. var cipher = create_cipher(shared_secret)
  73. var ephem_pub = random_ephem_key.key2hash(){:pub}
  74. var dest_pub = public_key.key2hash(){:pub}
  75. var :info = (
  76. dest => dest_pub,
  77. cipher => CONFIG{:cipher},
  78. chain_mode => CONFIG{:chain_mode},
  79. ephem_pub => ephem_pub,
  80. )
  81. var json = %S<JSON::PP>.encode_json(info)
  82. STDOUT.syswrite(pack("N*", json.len))
  83. STDOUT.syswrite(json)
  84. cipher.start('encrypting')
  85. while (fh.sysread(\(var buffer), BUFFER_SIZE)) {
  86. STDOUT.syswrite(cipher.crypt(buffer) \\ '')
  87. }
  88. STDOUT.syswrite(cipher.finish)
  89. }
  90. func decrypt (fh, private_key) {
  91. if (!defined(private_key)) {
  92. die "No private key provided!\n"
  93. }
  94. fh.sysread(\(var json_length), 32 >> 3)
  95. fh.sysread(\(var json), unpack("N*", json_length))
  96. var enc = %S<JSON::PP>.decode_json(json)
  97. # Make sure the private key is correct
  98. if (enc{:dest} != private_key.key2hash(){:pub}) {
  99. die "Incorrect private key!\n"
  100. }
  101. # The ephemeral public key
  102. var ephem_pub = enc{:ephem_pub}
  103. # Import the public key
  104. var ephem_pub_key = x25519_from_public(ephem_pub);
  105. # Recover the shared secret
  106. var shared_secret = private_key.shared_secret(ephem_pub_key)
  107. # Create the cipher
  108. var cipher = create_cipher(shared_secret, enc{:cipher}, enc{:chain_mode})
  109. cipher.start('decrypting')
  110. while (fh.sysread(\(var buffer), BUFFER_SIZE)) {
  111. STDOUT.syswrite(cipher.crypt(buffer) \\ '')
  112. }
  113. STDOUT.syswrite(cipher.finish)
  114. }
  115. func export_key (x_public_key) {
  116. Num(x_public_key, 16).base(EXPORT_KEY_BASE)
  117. }
  118. func decode_exported_key (public_key) {
  119. Num(public_key, EXPORT_KEY_BASE).as_hex
  120. }
  121. func decode_public_key (key) {
  122. x25519_from_public(decode_exported_key(key))
  123. }
  124. func decode_private_key (file) {
  125. file = File(file)
  126. if (!file.is_text) {
  127. die "Invalid key file!\n"
  128. }
  129. var key = %S<JSON::PP>.decode_json(file.read(:utf8))
  130. x25519_from_private(decode_exported_key(key{:x_priv}))
  131. }
  132. func generate_new_key {
  133. var x25519_key = x25519_random_key()
  134. var x_key = x25519_key.key2hash
  135. var x_public_key = x_key{:pub}
  136. var x_private_key = x_key{:priv}
  137. var :info = (
  138. x_pub => export_key(x_public_key),
  139. x_priv => export_key(x_private_key),
  140. )
  141. say %S<JSON::PP>.encode_json(info)
  142. STDERR.printf("Public key: %s\n", info{:x_pub})
  143. return 1;
  144. }
  145. func help (exit_code) {
  146. var chaining_modes = %w(cbc pcbc cfb ofb ctr).sort.map{.uc}
  147. var valid_ciphers = %w(
  148. AES Anubis Twofish Camellia Serpent SAFERP
  149. ).sort
  150. print <<"EOT"
  151. usage: #{__MAIN__} [options] [<input] [>output]
  152. Encryption and signing:
  153. -g --generate-key : Generate a new key-pair
  154. -e --encrypt=key : Encrypt data with a given public key
  155. -d --decrypt=key : Decrypt data with a given private key file
  156. --cipher=s : Change the symmetric cipher (default: #{CONFIG{:cipher}})
  157. valid: #{valid_ciphers.join(' ')}
  158. --chain-mode=s : Change the chaining mode (default: #{CONFIG{:chain_mode}})
  159. valid: #{chaining_modes.join(' ')}
  160. Examples:
  161. # Generate a key-pair
  162. #{__MAIN__} -g > key.txt
  163. # Encrypt a message for Alice
  164. #{__MAIN__} -e=RBZ17knALkL5N1AWYjAgBwZDpQpQmvLbuTphVAx7XQC < message.txt > message.enc
  165. # Decrypt a received message
  166. #{__MAIN__} -d=key.txt < message.enc > message.txt
  167. EOT
  168. Sys.exit(exit_code)
  169. }
  170. func version {
  171. printf("%s %s\n", SHORT_APPNAME, VERSION);
  172. Sys.exit(0)
  173. }
  174. ARGV.getopt!(
  175. 'cipher=s' => \CONFIG{:cipher},
  176. 'chain-mode|mode=s' => \CONFIG{:chain_mode},
  177. 'g|generate-key!' => \CONFIG{:generate_key},
  178. 'e|encrypt=s' => \CONFIG{:encrypt},
  179. 'd|decrypt=s' => \CONFIG{:decrypt},
  180. 'v|version' => version,
  181. 'h|help' => func { help(0) },
  182. )
  183. if (CONFIG{:generate_key}) {
  184. generate_new_key()
  185. Sys.exit(0)
  186. }
  187. func get_input_fh {
  188. var fh = STDIN
  189. if (ARGV and fh.is_on_tty) {
  190. File(ARGV[0]).sysopen(\(var file_fh), 0) ||
  191. die "Can't open file <<#{ARGV[0]}>> for reading: $!"
  192. return file_fh
  193. }
  194. return fh
  195. }
  196. if (defined(CONFIG{:encrypt})) {
  197. var x_pub = decode_public_key(CONFIG{:encrypt})
  198. encrypt(get_input_fh(), x_pub)
  199. Sys.exit(0)
  200. }
  201. if (defined(CONFIG{:decrypt})) {
  202. var x_priv = decode_private_key(CONFIG{:decrypt})
  203. decrypt(get_input_fh(), x_priv)
  204. Sys.exit(0)
  205. }
  206. help(1)