#!/usr/bin/perl # # encrypt [-dvx] < file_in > file_out # # WARNING: This program has been superseeded by the simplier "keepout" script. # # Pipelined encrypt, decrypt, using a method simular to that used by the # OpenSSL 'enc' command, and the "aespipe" pipeline encryption command. # However this program uses "Password Based Key Derivation Function v2" (or # PBKDF2 as per RFC2898) for the passphrase to key conversion, which is # designed to slow down brute force attacks, using interative hashed # functions. The other two do not have this security enhancement. # # The password is either read directly from /dev/tty, or by running # a "askpass" password reading program (see "Environment" below). The files # to encrypt/decrypt are then read in from STDIN or STDOUT (pipeline). # # Main Options # -d Decode the pipeline stream, default is to encode # -v Report major (time consuming) steps while processing # -vv Verbose debugging of lower level steps being performed # -x Get password via X windows. # --magic MAGIC Use this for the file magic string (generally 8 characters) # --key KEY Cache/Retrieve the password using KEY (via "askpass_stars") # --clear Clear the given KEY from cache - no other action # --help Print this documentation # # --pass file:FILE Read password from first line of this file # --pass fd:N Read password from this file descriptor # (WARNING: do not read from fd:0 or stdin) # # Password Caching Options # # The "--key" option enables the use of the password caching options added # to my own "askpass_stars" script, which allow the decryption password to # temporarily be cached in the Linux Kernel Keyring, so that it can be reused # later when re-encrypting the file, correctly. A key (typically the # filename, buffer id or process id) is required. # # This was added to reduce mishaps that was occurring when re-encrypting a # file that is being edited (using 'vim', see below), causing the file on disk # to be saved with the wrong password, and thus its contents lost (restore from # backup needed). # # The password should be immeditally cleared from the password cache when file # editing is complete (rather than left to timeout), which is why the # "--clear" option was also added. # # Example Usage... # # # Encrypt-decrypt pipeline (password given 3 times) # echo "This is a test message" | encrypt | encrypt -d # # # Create (then edit) a encrypted file # echo "This is a test message" > test.txt # encrypt < test.txt > test.enc # Encrypt file # shred test.txt # securely delete the plain text file # encrypt -d < test.enc # Contents of encrypted file # # # Editing file -- caching the password, so it is only asked once! # encrypt -d --key "cache_test_pwd" < test.enc > test.txt # vim test.txt # encrypt --key "cache_test_pwd" < test.txt > test.enc # encrypt --clear --key "cache_test_pwd" # shred test.txt # # You can also use this "vim" code to let you create and directly edit ".enc" # files, encrypted with this program. # https://antofthy.gitlab.io/software/#encrypt.vim # # Environment... # # You can specify a program to use to either 'ask' password from the user, # or from some other special source (like the gnome keyring) using the # environment variables... # TTY_ASKPASS # X_ASKPASS # The which one is selected depends on if a command line TTY is available, and # if "$DISPLAY" environment variable is set, or you force the use of X windows # over TTY by using a "-x" option. # # For example, to use a X Windows popup password reader you can use # X_ASKPASS=/usr/libexec/openssh/x11-ssh-askpass # encrypt -dx plain_text_file # # Also see my file encryption notes on # https://antofthy.gitlab.io/info/crypto/file_encrypt.txt # # Downloading... # # The latest version of the script is available at # https://antofthy.gitlab.io/software#encrypt # ### # # This program will also automatically decrypt a standard OpenSSL 'enc', if it # sees the openssl "Salted__" file magic. # # However since openssl v1.1.0 the standard options have changed, so can no # longer be considered 'standard'. # # Also PBKDF2 has become standard, and can be used from openssl command line. # This has removed the need to DIY the algorithm in perl. # # See the "keepout" script for an simpler alternative with the new "openssl" # standards. # https://antofthy.gitlab.io/software/#keepout # ### # # Internals... # # Major differences to (what was) a standard openssl encrypted file format # using the "openssl enc" command (see openssl manpage "enc"), are... # # * Different 'file magic' (or none at all) depending on settings below. # Openssl uses a 8 character magic string of "Salted__". By default we # use "PBKDF2__" but you can make your own 'magic'. # # * This is followed, by the random salt that was used during encryption # (16 bytes, openssl only used used 8 bytes). # # * After the salt an iterative count (ic) is stored in the file (4 bytes) # This is not provided by "OpenSSL enc", and is the major difference, # between this and the openssl file encryption (until openssl v1.1.1). # # * Hardcoded AES encryption as per the "Crypt::OpenSSL::AES" module. # Which is equivalent to using openssl "aes-256-cbc". Changing this # is a trivial modification to the script below. # # * The Password is PBKDF2 hashed and salted according to the count to slow # down generation of the decryption key, so that it takes approximately # 1/2 seconds to generate the decryption key. This makes brute force # dictionary attacks impractical. # # Note that the default openssl encryption technique uses single run # salted password hash method that makes brute force dictionary attacks # quite feasible, even with salting, and thus should be avoided. This is # the reason this program was created. # # This problem was not fixed in openssl for years. # # IN summery the encrypted file format is... # * 8 bytes of file magic (Optional) identifying encryption style. # * 16 bytes of random salt # * 4 bytes for the ic count in network number format # * Followed by the "aes-256-cbc" encrypted data blocks until EOF # ### # # Anthony Thyssen 13 November 2009 Anthony.Thyssen@gmail.com # # Version 1 13 November 2009 Initial Version # Used a external program to access an OpenSSL library function # PKCS5_PBKDF2_HMAC_SHA1 to implement PBKDF2 (interative HMAC). # Version 2 22 May 2014 # Added use of a perl PBKDF2 function to remove external dependancy # Version 3 11 December 2015 # Attempt at adding a checksum for password verification (later removed) # Version 4 11 December 2015 # OpenSSL command alternatives added (for compatibility testing) # Version 5 8 February 2017 # Use SSH_ASKPASS or TTY_ASKPASS envvars for password reading helpers # Version 6 13 October 2017 # Added options to use password caching, and increased iteration count # Version 7 26 May 2020 # Added openssl '-pass' option. # Version 8 26 May 2023 # Fixed "uninitialized value in print" error on short file encryptions. # # Script is now classed as obsolete, and has been supersedded by the simplier # "keepout" shell script wrapper around "openssl" (v1.1.1 and greater). # #### use strict; use warnings; use FindBin; my $PROGNAME = $FindBin::Script; use IPC::Open2; use Crypt::CBC; use Digest::HMAC_SHA1 qw(hmac_sha1); # we probably should switch to sha256 #use Digest::SHA qw(sha1); # Magic Prefix (default) - if you really must identify encrypted files # internally This should be either a undefined, or 8 character string. # However code will accept any file magic string of less than 20 bytes. # #my $magic = ""; # Do not encrypt with any file magic! #my $magic = "Salted__"; # pretend it is a openssl encryption! (DO NOT USE) my $magic = "PBKDF2__"; # A logical file magic for this type of encryption. #my $magic = "KeepOut_"; # A more personalized bit of magic! # Iteration count for PBKDF2 hashing (bigger is better but takes longer) my $ic_minimum = 100000; # warn user if iteration count is smaller than this my $ic_generate = 200000; # minimum number of iterations when encrypting # maximum number is twice this value. # Selection of the cryptography method to use. my $CryptModule = "Crypt::OpenSSL::AES"; # Advanced Encryption Standard #my $CryptModule = "Crypt::Rijndael"; # Original 'AES' method #my $CryptModule = "Crypt::Blowfish"; # Blowfish 'XOR' Encryption #my $CryptModule = "Crypt::DES"; #my $CryptModule = "Crypt::IDEA"; my $READ_SIZE = 4096; # buffer size to data when reading sub Usage { print STDERR "$PROGNAME: ", @_, "\n"; @ARGV = ( "$FindBin::Bin/$PROGNAME" ); # locate script file while( <> ) { next if 1 .. 2; last if /^###/; s/^#$//; s/^# //; last if /^$/; # end after usage lines print STDERR "Usage: " if 3 .. 3; print STDERR; } print STDERR "For full manual use --help\n"; exit 10; } sub Help { @ARGV = ( "$FindBin::Bin/$PROGNAME" ); # locate script file while( <> ) { next if $. == 1; last if /^###/; last unless /^#/; s/^#$//; s/^# //; print STDERR; } exit 10; } sub Fail { print STDERR "$@"; exit 1; } my $verbose = 0; sub Verbose { print STDERR @_, "...\n" if $verbose == 1; } sub Debug { print STDERR "PROGNAME debug: ", @_, "...\n" if $verbose > 1; } # ------------------------------------------------------------------------- # Argument handling my $decrypt = 0; my $askpass_x = 0; my $keyname; my $clear_cache=0; my $pass = undef; OPTION: # Multi-switch option handling while( @ARGV && $ARGV[0] =~ s/^-(?=.)// ) { $_ = shift; { m/^$/ && do { next }; # Next option m/^-$/ && do { last }; # End of options '--' m/^\?/ && do { Help }; # Usage Help '-?' m/^-?(help|doc|man|manual)$/ && Help; # Print help manual comments s/^d// && do { $decrypt++; redo }; # decrypt the file s/^v// && do { $verbose++; redo }; # verbose step reporting s/^x// && do { $askpass_x++; redo }; # get password via X windows m/^-?key(|name)$/ && do {$keyname = shift; next }; # enable password cache m/^-?clear$/ && do {$clear_cache++; next }; # clear password cache m/^-?magic$/ && do {$magic = shift; next }; # file magic string m/^-?pass$/ && do { # read password from file or stream $pass = shift; $pass =~ s/^fd:/file:&/; # password from file descriptor $pass =~ s/^file:// or Usage("unknown '--pass' option"); next }; Usage( "Unknown Option \"-$_\"" ); } continue { next OPTION }; last OPTION; } Usage( "Too Many Arguments" ) if @ARGV; # # Just clear the given Password Cache (password no longer needed) # if ( $clear_cache ) { Usage( "Missing keyname for \"--clear\"" ) unless defined $keyname; if ( defined $ENV{TTY_ASKPASS} && $ENV{TTY_ASKPASS} =~ m/(^|\/)askpass_stars$/ ) { system($ENV{TTY_ASKPASS}, "--clear-cached", "--keyname", "$keyname"); } if ( defined $ENV{TTY_ASKPASS} && $ENV{TTY_ASKPASS} =~ m/(^|\/)systemd-ask-password$/ ) { system("/bin/keyctl", "purge", "user", $keyname); } exit 0; } # ----------------------------------------------------------------- # Subroutines sub passwd_read { my $p; if ( defined $pass ) { # Read password from a file or file descriptor. # This will still read a large buffer from a file descriptor, not just # the first line, as such it is not suitable for reading from 'stdin'. open(P, "<$pass") or Fail("Open password file or stream: $!\n"); $p =

; close P; Fail("Password failed to be read\n") unless length $p; } elsif ( $askpass_x ) { # Use a GUI Password Helper to read password #Set_Focus('KS', 0); if ( defined $ENV{X_ASKPASS} ) { $p = `$ENV{X_ASKPASS} "@_"` } elsif ( defined $ENV{SSH_ASKPASS} ) { $p = `$ENV{SSH_ASKPASS} "@_"` } else { # fall back to something that should be present $p = #`zenity --title=KS --entry --text="@_" --hide-text` #`Xdialog --title KS --stdout --password --inputbox "@_" 0x0` `/usr/libexec/openssh/x11-ssh-askpass -title KS "@_"` #`askpass_x KS "@_"` ; } } else { # Use a TTY Password Helper to read password. # I actually don't like these 'curses' based password helpers, such as # "dialog" or "whiptail", as they take over the whole TTY window. # They also leave the decrypted file on screen when used with "vim". if ( defined $ENV{TTY_ASKPASS} ) { @_=("'@_'"); # add quotes to password prompt # enable password caching (depends on en/de-crypt mode) if ( defined $keyname && $ENV{TTY_ASKPASS} =~ m/(^|\/)askpass_stars$/ ) { unshift(@_, ($decrypt?"--keyname":"--retrieve"), "'$keyname'"); } #print STDERR "DEBUG: $ENV{TTY_ASKPASS} @_\n"; $p = `$ENV{TTY_ASKPASS} @_ &1 >/dev/tty` # `askpass_stars @_ /dev/null) ); system('stty', '-F', '/dev/tty', '-echo'); # turn off echo print STDERR @_; open(TTY, "/dev/tty"); chomp( my $p = ); close TTY; system('stty', '-F', '/dev/tty', $stty_reset); print STDERR "\n"; } chomp( $p ); # remove the final newline # print STDERR "Password: $p\n"; # debugging return $p; } # Password Iterative Hashing Function # # I would prefer to use the openssl library function PKCS5_PBKDF2_HMAC_SHA1, # but the "openssl" command does not provide access to it, and the perl # openssl module has a very large module dependance overhead. As such # I instead am relying on a DIY pure-perl equivelent function. # # Function by Jochen Hoenicke from the # Palm::Keyring perl module. Found on the PerlMonks Forum # http://www.perlmonks.org/?node_id=631963 # # UPDATE: openssl now provide access to the function, essentualy obsoleteing # this script. See the simpler "keepout" script! # # Usage pbkdf2(prf, password, salt, iter, keylen) sub pbkdf2 { my ($prf, $password, $salt, $iter, $keylen) = @_; my ($k, $t, $u, $ui, $i); $t = ""; for ($k = 1; length($t) < $keylen; $k++) { $u = $ui = &$prf($salt.pack('N', $k), $password); for ($i = 1; $i < $iter; $i++) { $ui = &$prf($ui, $password); $u ^= $ui; } $t .= $u; } return substr($t, 0, $keylen); } # Wrapper around the above to use the "hmac_sha1" hashing function # # Ideally we should be able to access the openssl library function directly # via the "openssl" command, rather that these to round-a-bout methods. # # UPDATE: with openssl v1.1.1 you now do have access but it was limited to # a 8 byte salt, where I used a 16 byte salt (unfortunate design choice). # # Usage pbkdf2_sha1( password, salt, iter ) sub pbkdf2_sha1 { #print STDERR "DEBUG: pbkdf2 ", unpack("H*", $_[1]), " $_[2]\n"; my $k; if( 1 ) { # Use the above function, specify final key length # and the 'sha1' hashing from the DIGEST perl module $k = pbkdf2(\&hmac_sha1, @_, 32+16); } else { # Use a C wrapper that breaks out the function from the OpenSSL Library local( *O, *I ); my $pid = open2( \*O, \*I, 'pbkdf2', unpack("H*", $_[1]), $_[2] ); print I $_[0]; close I; chomp( $k = ); close 0; waitpid($pid, 0); die( "Program 'pbkdf2' returned bad status : ", ($? >> 8), "\n" ) if $?; $k = pack("H*", $k); } #print STDERR unpack("H*",$k), "\n"; return( wantarray ? (substr($k,0,32), substr($k,32,16) ) : $k ); } # ----------------------------------------------------------------- # # Encrypt a file stream # if ( ! $decrypt ) { # Generate random (public) constants for this specific encryption my $salt=`openssl rand 16`; # generate random salt my $ic = $ic_generate + int(rand $ic_generate); # iterative hash count # Future retrieve the password to encrypt with from password cache! # Read Password for encryption my $p; while ( 1 ) { $p = passwd_read "Encryption Password :"; # spaced out so it is, if ( ! length $p ) {; print STDERR "Zero length Password not allowed -- Try Again!\n"; next; } last if defined $pass; my $p2 = passwd_read "Encryption Pwd Again:"; # the same length as this. last if $p eq $p2; print STDERR "Password Mismatch -- Try Again!\n"; } # Special Case... # The password was to have come from the cache, but the key expired, # So a password was asked from the user, and BOTH of them match. # Re-save the password into the password cache -- again! # Also refresh the timeout of any existing key cache if ( defined $keyname && $ENV{TTY_ASKPASS} =~ m/(^|\/)askpass_stars$/ ) { my $key_id=`keyctl request user "$keyname" 2>/dev/null`; if ( !$key_id ) { # if key is no longer cached, re-save the key into cache $key_id=`echo -n "$p" | keyctl padd user "$keyname" \@u 2>/dev/null`; } system("keyctl timeout '$key_id' 1800 2>/dev/null") if $key_id; } # Password + Salt + IC -> Cryptographic Key and IV Verbose "Encoding Key"; # this is where most time is being spent! my ($k,$iv) = pbkdf2_sha1( $p, $salt, $ic ); # Output the header: optional magic, salt, iterative hash count print $magic; # optional file magic (typically 0 or 8 bytes) print $salt; # salt as 16 byte raw binary string print pack("N", $ic); # count as 4 byte network number if ( 1 ) { # Encrypt the data: stdin to stdout # The header was already output Verbose "Encrypting Data"; my $cipher = Crypt::CBC->new( -cipher => $CryptModule, -header => 'none', -literal_key => 1, -key => $k, -iv => $iv ); my $buffer; $cipher->start('encrypting'); print $cipher->crypt(""); # precaution against an empty file while (my $len = read(STDIN,$buffer,$READ_SIZE)) { #print STDERR "length=$len error=$!\n"; # debugging short files print $cipher->crypt($buffer) || ""; } print $cipher->finish; } else { # # Encrypt the data: stdin to stdout using openssl (compatibility testing) # # Use 'openssl enc' command to encrypt the data stream. We have already # output the 28 byte header, so the command only needs to the encrypted # data, using the already decoded Cryptographic Key and IV # # For file comparision the salt and hash count (ic) needs to be fixed, # so the key and iv becomes fixed. # # WARNING: This should not be used as is. It exposes the key in the process # table. That part should be replaced with a more secure method, provided # by the "openssl" command. # Verbose "Encrypting Data (via openssl)"; system( qw( openssl aes-256-cbc ), '-K', unpack("H*",$k), '-iv', unpack("H*",$iv) ); } Debug "Encryption Complete"; exit 0; # finished Encrypting } # ----------------------------------------------------------------- # # Decrypt a file stream # my $buffer; Debug "Reading encrypted file header"; # Read header my $ml = length($magic); # magic_length (really should be just 8) my $hl = $ml+16+4; # header_length (magic + salt + iteration count read(STDIN,$buffer,$hl) == $hl # Expected header length or die("$PROGNAME: Encrypted file is not big enough for just the header!\n"); # # Handle different types of file magic -- modify to suit. # # We are not 'faking' 'openssl enc' files, and its file magic is present. if ( $magic ne "Salted__" && substr($buffer,0,8) eq "Salted__" ) { # Assuming file is a normal openssl encrypted file # assume AES encryption which is (fairly standard) if ( 1 ) { # Use the Crypt::CBC command to decrypt a openssl encrypted file my $p = passwd_read "\nDecryption Password :"; Verbose "Decrypting OpenSSL Data"; my $cipher = Crypt::CBC->new( -salt => 1, # special flag - openssl salted file -cipher => "Crypt::OpenSSL::AES", -key => $p ); undef $p; $cipher->start('decrypting'); print $cipher->crypt($buffer); # Give it the magic & salt while (read(STDIN,$buffer, $READ_SIZE)) { print $cipher->crypt($buffer); } print $cipher->finish; # output final block } else { # Otherwise: # Use the external 'openssl enc' command to decrypt the data stream. # This does exactly the same thing as the above! # Verbose "Decrypting OpenSSL Data (via openssl)"; system( qw( openssl enc -d -aes-256-cbc ) ); # OpenSSL has changed... # # Unless OpenSSL v1.1.0 was used where "-md md5" default changed. # Unless OpenSSL v1.1.1 was used with the new PDKDF2 password hashing. # # Basically there are now too many options to know what the "Salted__" # file magic actually means with regards to a encrypted file, without extra # metadata associated with the file. # # See the OpenSSL wrapper script "keepout", as an alternative # https://antofthy.gitlab.io/software/#keepout # } exit; # Finished decrypting file } # check file magic is appropriate (ml = magic length) elsif ( $ml && substr($buffer,0,$ml) ne $magic ) { die("$PROGNAME: Encrypted file has invalid file magic!\n"); } # Do we need to read more data into our buffer? # # At this time we are not looking for any other information so... NO. # # Really we should have read a minimum amount needed to determine the # encryption method used. Once that was determined we can then read enough # to get the full header needed to decode the encrypted data that follows. # (File magic, then 16 for Salt and a 4 bytes IC count) # Extract the 16 byte salt, and the 4 byte iterative count my $salt = substr($buffer, $ml+0, 16); my $ic = unpack("N", substr($buffer, $ml+16, 4) ); #print STDERR "salt=", unpack("H*",$salt), "\n"; #print STDERR "ic=", $ic, "\n"; #printf STDERR "DEBUG: $ic_minimum <= $ic < %d\n", $ic_generate+$ic_generate; Debug "Decryption PBKDF2 Iterations $ic"; # Read Password my $p = passwd_read "Decryption Password :"; #print "p=", $p, "\n"; # Sanity check of the iterative count (ic) -- after we read the password Verbose "Decoding Key"; # This is where most time is being spent ( $ic < $ic_minimum ) and warn("$PROGNAME: Encrypted file has a low iteration count!\n"); #( $ic > $ic_generate+$ic_generate ) # and die("$PROGNAME: Encrypted filehas a very large iteration count)!\n"); # Password + Salt (iterated IC times) -> Cryptographic Key and IV my ($k,$iv) = pbkdf2_sha1( $p, $salt, $ic ); $p = "Password is no longer needed!"; # try to destroy it (probably won't) #print STDERR "key=", unpack("H*",$k), "\n"; # very low level debug #print STDERR "iv=", unpack("H*",$iv), "\n"; # Decryption method - perl Crypt, or "openssl" if ( 1 ) { # Decrypt the data: stdin to stdout # The header was already done. Verbose "Decrypting Data"; my $cipher = Crypt::CBC->new( -cipher => $CryptModule, -header => 'none', -literal_key => 1, -key => $k, -iv => $iv ); $cipher->start('decrypting'); while (read(STDIN,$buffer, $READ_SIZE)) { print $cipher->crypt($buffer); } print $cipher->finish; } else { # Decrypt the data: stdin to stdout using openssl (compatibility testing) # # Use the 'openssl enc' command to decrypt the data stream. We have already # read the full 28 byte header, so the command only sees the encrypted data, # and decrypts it using the already decoded Cryptographic Key and IV. # Verbose "Decrypting Data (via openssl)"; system( qw( openssl aes-256-cbc -d ), '-K', unpack("H*",$k), '-iv', unpack("H*",$iv) ); } # FUTURE: Save the input password into the password cache for later retrieval Debug "Decryption Complete"; exit 0;