age-lf.pl 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. #!/usr/bin/perl
  2. # Author: Trizen
  3. # Date: 02 February 2022
  4. # Edit: 09 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. use 5.020;
  14. use strict;
  15. use warnings;
  16. use experimental qw(signatures);
  17. use Crypt::CBC;
  18. use Crypt::PK::X25519;
  19. use JSON::PP qw(encode_json decode_json);
  20. use Getopt::Long qw(GetOptions :config no_ignore_case);
  21. binmode(STDIN, ':raw');
  22. binmode(STDOUT, ':raw');
  23. use constant {
  24. SHORT_APPNAME => "age-lf",
  25. BUFFER_SIZE => 1024 * 1024,
  26. EXPORT_KEY_BASE => 62,
  27. VERSION => '0.01',
  28. };
  29. my %CONFIG = (
  30. cipher => 'Serpent',
  31. chain_mode => 'CBC',
  32. );
  33. sub create_cipher ($pass, $cipher = $CONFIG{cipher}, $chain_mode = $CONFIG{chain_mode}) {
  34. Crypt::CBC->new(
  35. -pass => $pass,
  36. -cipher => 'Cipher::' . $cipher,
  37. -chain_mode => lc($chain_mode),
  38. -pbkdf => 'pbkdf2',
  39. );
  40. }
  41. sub x25519_from_public ($hex_key) {
  42. Crypt::PK::X25519->new->import_key(
  43. {
  44. curve => "x25519",
  45. pub => $hex_key,
  46. }
  47. );
  48. }
  49. sub x25519_from_private ($hex_key) {
  50. Crypt::PK::X25519->new->import_key(
  51. {
  52. curve => "x25519",
  53. priv => $hex_key,
  54. }
  55. );
  56. }
  57. sub x25519_random_key {
  58. while (1) {
  59. my $key = Crypt::PK::X25519->new->generate_key;
  60. my $hash = $key->key2hash;
  61. next if substr($hash->{pub}, 0, 1) eq '0';
  62. next if substr($hash->{priv}, 0, 1) eq '0';
  63. next if substr($hash->{pub}, -1) eq '0';
  64. next if substr($hash->{priv}, -1) eq '0';
  65. return $key;
  66. }
  67. }
  68. sub encrypt ($fh, $public_key) {
  69. # Generate a random ephemeral key-pair.
  70. my $random_ephem_key = x25519_random_key();
  71. # Create a shared secret, using the random key and the reciever's public key
  72. my $shared_secret = $random_ephem_key->shared_secret($public_key);
  73. my $cipher = create_cipher($shared_secret);
  74. my $ephem_pub = $random_ephem_key->key2hash->{pub};
  75. my $dest_pub = $public_key->key2hash->{pub};
  76. my %info = (
  77. dest => $dest_pub,
  78. cipher => $CONFIG{cipher},
  79. chain_mode => $CONFIG{chain_mode},
  80. ephem_pub => $ephem_pub,
  81. );
  82. my $json = encode_json(\%info);
  83. syswrite(STDOUT, pack("N*", length($json)));
  84. syswrite(STDOUT, $json);
  85. $cipher->start('encrypting');
  86. while (sysread($fh, (my $buffer), BUFFER_SIZE)) {
  87. syswrite(STDOUT, $cipher->crypt($buffer) // '');
  88. }
  89. syswrite(STDOUT, $cipher->finish);
  90. }
  91. sub decrypt ($fh, $private_key) {
  92. if (not defined $private_key) {
  93. die "No private key provided!\n";
  94. }
  95. if (ref($private_key) ne 'Crypt::PK::X25519') {
  96. die "Invalid private key!\n";
  97. }
  98. sysread($fh, (my $json_length), 32 >> 3);
  99. sysread($fh, (my $json), unpack("N*", $json_length));
  100. my $enc = decode_json($json);
  101. # Make sure the private key is correct
  102. if ($enc->{dest} ne $private_key->key2hash->{pub}) {
  103. die "Incorrect private key!\n";
  104. }
  105. # The ephemeral public key
  106. my $ephem_pub = $enc->{ephem_pub};
  107. # Import the public key
  108. my $ephem_pub_key = x25519_from_public($ephem_pub);
  109. # Recover the shared secret
  110. my $shared_secret = $private_key->shared_secret($ephem_pub_key);
  111. # Create the cipher
  112. my $cipher = create_cipher($shared_secret, $enc->{cipher}, $enc->{chain_mode});
  113. $cipher->start('decrypting');
  114. while (sysread($fh, (my $buffer), BUFFER_SIZE)) {
  115. syswrite(STDOUT, $cipher->crypt($buffer) // '');
  116. }
  117. syswrite(STDOUT, $cipher->finish);
  118. }
  119. sub export_key ($x_public_key) {
  120. require Math::BigInt;
  121. Math::BigInt->from_hex($x_public_key)->to_base(EXPORT_KEY_BASE);
  122. }
  123. sub decode_exported_key ($public_key) {
  124. require Math::BigInt;
  125. Math::BigInt->from_base($public_key, EXPORT_KEY_BASE)->to_hex;
  126. }
  127. sub decode_public_key ($key) {
  128. x25519_from_public(decode_exported_key($key));
  129. }
  130. sub decode_private_key ($file) {
  131. if (not -T $file) {
  132. die "Invalid key file!\n";
  133. }
  134. open(my $fh, '<:utf8', $file)
  135. or die "Can't open file <<$file>>: $!";
  136. local $/;
  137. my $key = decode_json(<$fh>);
  138. x25519_from_private(decode_exported_key($key->{x_priv}));
  139. }
  140. sub generate_new_key {
  141. my $x25519_key = x25519_random_key();
  142. my $x_key = $x25519_key->key2hash;
  143. my $x_public_key = $x_key->{pub};
  144. my $x_private_key = $x_key->{priv};
  145. my %info = (
  146. x_pub => export_key($x_public_key),
  147. x_priv => export_key($x_private_key),
  148. );
  149. say encode_json(\%info);
  150. warn sprintf("Public key: %s\n", $info{x_pub});
  151. return 1;
  152. }
  153. sub help ($exit_code) {
  154. local $" = " ";
  155. my @chaining_modes = map { uc } qw(cbc pcbc cfb ofb ctr);
  156. my @valid_ciphers = sort grep {
  157. eval { require "Crypt/Cipher/$_.pm"; 1 };
  158. } qw(
  159. AES Anubis Twofish Camellia Serpent SAFERP
  160. );
  161. print <<"EOT";
  162. usage: $0 [options] [<input] [>output]
  163. Encryption and signing:
  164. -g --generate-key : Generate a new key-pair
  165. -e --encrypt=key : Encrypt data with a given public key
  166. -d --decrypt=key : Decrypt data with a given private key file
  167. --cipher=s : Change the symmetric cipher (default: $CONFIG{cipher})
  168. valid: @valid_ciphers
  169. --chain-mode=s : Change the chaining mode (default: $CONFIG{chain_mode})
  170. valid: @chaining_modes
  171. Examples:
  172. # Generate a key-pair
  173. $0 -g > key.txt
  174. # Encrypt a message for Alice
  175. $0 -e=RBZ17knALkL5N1AWYjAgBwZDpQpQmvLbuTphVAx7XQC < message.txt > message.enc
  176. # Decrypt a received message
  177. $0 -d=key.txt < message.enc > message.txt
  178. EOT
  179. exit($exit_code);
  180. }
  181. sub version {
  182. my $width = 20;
  183. printf("%-*s %s\n", $width, SHORT_APPNAME, VERSION);
  184. printf("%-*s %s\n", $width, 'Crypt::CBC', $Crypt::CBC::VERSION);
  185. printf("%-*s %s\n", $width, 'Crypt::PK::X25519', $Crypt::PK::X25519::VERSION);
  186. printf("%-*s %s\n", $width, 'Crypt::PK::Ed25519', $Crypt::PK::Ed25519::VERSION);
  187. exit(0);
  188. }
  189. GetOptions(
  190. 'cipher=s' => \$CONFIG{cipher},
  191. 'chain-mode|mode=s' => \$CONFIG{chain_mode},
  192. 'g|generate-key!' => \$CONFIG{generate_key},
  193. 'e|encrypt=s' => \$CONFIG{encrypt},
  194. 'd|decrypt=s' => \$CONFIG{decrypt},
  195. 'v|version' => \&version,
  196. 'h|help' => sub { help(0) },
  197. )
  198. or die("Error in command line arguments\n");
  199. if ($CONFIG{generate_key}) {
  200. generate_new_key();
  201. exit 0;
  202. }
  203. sub get_input_fh {
  204. my $fh = \*STDIN;
  205. if (@ARGV and -t $fh) {
  206. sysopen(my $file_fh, $ARGV[0], 0)
  207. or die "Can't open file <<$ARGV[0]>> for reading: $!";
  208. return $file_fh;
  209. }
  210. return $fh;
  211. }
  212. if (defined($CONFIG{encrypt})) {
  213. my $x_pub = decode_public_key($CONFIG{encrypt});
  214. encrypt(get_input_fh(), $x_pub);
  215. exit 0;
  216. }
  217. if (defined($CONFIG{decrypt})) {
  218. my $x_priv = decode_private_key($CONFIG{decrypt});
  219. decrypt(get_input_fh(), $x_priv);
  220. exit 0;
  221. }
  222. help(1);