#! /usr/bin/env perl # Simplified BSD License (FreeBSD License) # # Copyright (c) 2025, Daily Data Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use strict; use warnings; use Data::Dumper; use File::Path qw(make_path); use Getopt::Long qw(GetOptions); our $VERSION = '1.1'; # script will create the key files used for GELI encryption and other uses. # for security, the keys should be stored on a tmpfs or ramdisk so no record # is left on disk. # default is to generate a 256-bit (32-byte) key, then split into two local keys # using XOR, meaning both local keys can be used to reconstruct the master key. # the master key is can be used for encryption/decryption directly, but for added # security, it is recommended to use the local keys, providing each via separate means. # Example usage: # perl makeGeliKey # perl makeGeliKey --keysize 512 --keydir /tmp/keys/ --combined master.key --local1 part1.key --local2 part2.key # Now, encrypt a GELI device using: # geli init -B -p -k /tmp/keys/master.key # Provide one local key to one party, and the other local key to another party. # To reconstruct the master key, both local keys are required. Sample reconstruction using Perl (assumes 256-bit keys, 32 bytes each): # use strict; use warnings; # open(my $k1, "<:raw", "/tmp/keys/part1.key") or die $!; # open(my $k2, "<:raw", "/tmp/keys/part2.key") or die $!; # read($k1, my $key1, 32); # read($k2, my $key2, 32); # close($k1); close($k2); # my $master = $key1 ^ $key2; # open(my $mk, ">:raw", "/tmp/keys/master.key") or die $!; print $mk $master; close($mk); # use the reconstructed master key file to attach the GELI device: # geli attach -k /tmp/keys/master.key # # Additional notes: # if only the combined key file is provided, it will be created as a single key. # if both local key files are provided, the combined key will be split into # the two local keys using XOR, so that both local keys are required to # reconstruct the combined key. # # Key size can be specified in bits (128, 192, 256, 512, etc.) via --keysize option. # Default is 256 bits. If the combined key file already exists, its size is used. # Filenames can be absolute paths or relative to --keydir option. # Key directory should be on tmpfs or ramdisk for security. Don't leave keys on disk. # Revision History: # 2024-06-10 - RWR - version 1.0 # initial release # # 2025-12-22 - RWR - version 1.1 # Added command-line options for key file names and directory # Added help option and improved usage output # # these are the names of the key files used to create the GELI encryption # if both local key files are provided, the combined key will be split into # the two local keys using XOR, so that both local keys are required to # reconstruct the combined key. # if only the combined key file is provided, it will be created as a single key. my $rootKeyLocation = '/tmp/keys/'; # '/media/passkey/'; # should be a tmpfs or ramdisk so no record is left on disk my $localKeyFile1 = 'part1.key'; my $localKeyFile2 = 'part2.key'; my $combinedKeyFile = 'master.key'; # Default key size in bits (256 bits = 32 bytes for GELI) my $keySizeBits = 256; # Parse command-line options GetOptions( 'keysize=i' => \$keySizeBits, 'keydir=s' => \$rootKeyLocation, 'combined=s' => \$combinedKeyFile, 'local1=s' => \$localKeyFile1, 'local2=s' => \$localKeyFile2, 'version|V' => sub { print "makeGeliKey version $VERSION\n"; exit 0; }, 'help|h' => sub { print "Usage: $0 [OPTIONS]\n"; print "\nKey Generation Options:\n"; print " --keysize BITS Key size in bits (default: 256)\n"; print " Common values: 128, 192, 256, 512\n"; print "\nFile Location Options:\n"; print " --keydir PATH Directory for key files (default: /tmp/keys/)\n"; print " --combined NAME Combined key filename (default: master.key)\n"; print " --local1 NAME Local key 1 filename (default: part1.key)\n"; print " --local2 NAME Local key 2 filename (default: part2.key)\n"; print "\nOther Options:\n"; print " --version, -V Display version number\n"; print " --help, -h Display this help message\n"; print "\nNotes:\n"; print " - If master key file exists, its size will be used instead of --keysize\n"; print " - Filenames can be absolute paths or relative to --keydir\n"; print " - Key directory should be on tmpfs or ramdisk for security\n"; exit 0; }, ) or die "Error in command line arguments\n"; # Ensure rootKeyLocation ends with a slash $rootKeyLocation .= '/' unless $rootKeyLocation =~ m{/$}; # Build full paths for key files if they are not absolute paths $combinedKeyFile = $rootKeyLocation . $combinedKeyFile unless $combinedKeyFile =~ m{^/}; $localKeyFile1 = $rootKeyLocation . $localKeyFile1 unless $localKeyFile1 =~ m{^/}; $localKeyFile2 = $rootKeyLocation . $localKeyFile2 unless $localKeyFile2 =~ m{^/}; # Validate key size die "Key size must be a multiple of 8 bits\n" if $keySizeBits % 8 != 0; die "Key size must be at least 128 bits\n" if $keySizeBits < 128; # sub will take four parameters: # 1: combined key file to create # 2: local key file 1 to create (optional) # 3: local key file 2 to create (optional) # 4: key size in bytes # If both local key files are provided, the combined key will be split into # the two local keys using XOR, so that both local keys are required to # reconstruct the combined key. # returns the actual contents of the combined key, local key 1, and local key 2 as array sub makeKeys { $combinedKeyFile = shift; $localKeyFile1 = shift; $localKeyFile2 = shift; my $keySizeBytes = shift; die "No key size provided to makeKeys\n" unless defined $keySizeBytes; # the actual contents of each key file, returned as raw binary strings for further processing my $combinedKey; my $localKey1; my $localKey2; # create a new combined key file if it does not exist $combinedKey = makeKeyFile( $combinedKeyFile, $keySizeBytes ); # if both local key file names are provided, create them. if ( $localKeyFile1 && $localKeyFile2 ) { $localKey1 = makeKeyFile( $localKeyFile1, $keySizeBytes ); $localKey2 = xorKeys( $combinedKey, $localKey1 ); writeKeyfile( $localKeyFile2, $localKey2, $keySizeBytes ); } return ( $combinedKey, $localKey1, $localKey2 ); } # unpack and display a binary key as hex with a label sub displayKey { my ( $key, $label ) = @_; die "No key provided to displayKey\n" unless defined $key; die "No label provided to displayKey\n" unless defined $label; my $hex = unpack( 'H*', $key ); print "$label: $hex\n"; } sub runCommand { my $command = shift; print "$command\n"; `$command`; } sub readKeyfile { my $keyfile = shift; my $keySizeBytes = shift; # optional; if not provided, reads entire file die "No keyfile provided to readKeyfile\n" unless defined $keyfile; open( my $fh, '<:raw', $keyfile ) or die "Cannot open keyfile $keyfile: $!"; my $key; if (defined $keySizeBytes) { my $read = read($fh, $key, $keySizeBytes); close $fh; die "Failed to read $keySizeBytes bytes from $keyfile (got " . (defined $read ? $read : 0) . ")\n" unless defined $read && $read == $keySizeBytes; } else { # Read entire file to determine size local $/; $key = <$fh>; close $fh; die "Failed to read from $keyfile\n" unless defined $key; } return $key; # raw binary key } sub writeKeyfile { my ( $keyfile, $key, $keySizeBytes ) = @_; die "No keyfile provided to writeKeyfile\n" unless defined $keyfile; die "No key provided to writeKeyfile\n" unless defined $key; $keySizeBytes //= length($key); # default to actual key length open( my $fh, '>:raw', $keyfile ) or die "Cannot open keyfile $keyfile for writing: $!"; my $written = syswrite( $fh, $key ); close $fh; die "Failed to write $keySizeBytes bytes to $keyfile (wrote " . (defined $written ? $written : 0) . ")\n" unless defined $written && $written == $keySizeBytes; } sub xorKeys { my ( $key1, $key2 ) = @_; die "No key1 provided to xorKeys\n" unless defined $key1; die "No key2 provided to xorKeys\n" unless defined $key2; my $len1 = length($key1); my $len2 = length($key2); die "Keys must be the same length (got $len1 and $len2 bytes)\n" unless $len1 == $len2; my $result = $key1 ^ $key2; return $result; } # create a new random key file of specified size unless it already exists # returns the raw binary contents of the key sub makeKeyFile { my $keyfile = shift; my $keySizeBytes = shift; die "No keyfile name provided to makeKeyFile\n" unless defined $keyfile; die "No key size provided to makeKeyFile\n" unless defined $keySizeBytes; if ( -f $keyfile ) { print "Key file $keyfile already exists, not overwriting\n"; } else { print "Creating new key file $keyfile ($keySizeBytes bytes)\n"; if ( -x '/usr/bin/openssl' ) { # openssl prefers /dev/urandom automatically runCommand( "openssl rand -out $keyfile $keySizeBytes" ); } else { # dd displays too much output, so redirect to /dev/null (stdout and stderr) runCommand( "dd if=/dev/random of=$keyfile bs=$keySizeBytes count=1 >/dev/null 2>&1" ); } } return readKeyfile( $keyfile, $keySizeBytes ); } # ensure the root key location directory exists make_path( $rootKeyLocation ) unless -d $rootKeyLocation; # Determine key size in bytes my $keySizeBytes; if ( -f $combinedKeyFile ) { # If combined key file exists, determine size from it my $existingKey = readKeyfile( $combinedKeyFile ); $keySizeBytes = length($existingKey); my $detectedBits = $keySizeBytes * 8; print "Detected existing combined key file with size: $detectedBits bits ($keySizeBytes bytes)\n"; } else { # Convert bits to bytes $keySizeBytes = $keySizeBits / 8; print "Using key size: $keySizeBits bits ($keySizeBytes bytes)\n"; } # create the key(s) used for GELI encryption my ( $combinedKey, $localKey1, $localKey2 ) = makeKeys( $combinedKeyFile, $localKeyFile1, $localKeyFile2, $keySizeBytes ); # show the results as ASCII hex for recording print "Keys created, please record this sensitive information for your records:\n"; displayKey( $combinedKey, 'Combined Key' ); displayKey( $localKey1, 'Local Key 1' ) if defined $localKey1; displayKey( $localKey2, 'Local Key 2' ) if defined $localKey2;