Diff -- /var/www/vhosts/3dshawn.com/site1/MODS/Crypto.pm
Diff
/var/www/vhosts/3dshawn.com/site1/MODS/Crypto.pm
added on local at 2026-07-11 23:15:08
Added
+149
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to c0bc42cd7bdf
to c0bc42cd7bdf
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.| 1 | package MODS::Crypto; | |
| 2 | #====================================================================== | |
| 3 | # DriftSense -- MODS::Crypto | |
| 4 | # | |
| 5 | # Symmetric encryption of secrets (SSH private keys, webhook auth | |
| 6 | # tokens, etc.) so they can be stored in the DriftSense DB without | |
| 7 | # leaving plaintext at rest. | |
| 8 | # | |
| 9 | # Design: shells out to /usr/bin/openssl. No CPAN modules required. | |
| 10 | # AES-256-CBC + PBKDF2 (100k iterations), random per-payload salt, base64 | |
| 11 | # output. Master secret is passed via `-pass file:PATH` where PATH is | |
| 12 | # a mode-0600 temp file that exists only for the duration of the call | |
| 13 | # (safe from `ps` and env-var snooping). | |
| 14 | # | |
| 15 | # Public API: | |
| 16 | # | |
| 17 | # my $ciphertext_b64 = MODS::Crypto::encrypt($plaintext); | |
| 18 | # my $plaintext = MODS::Crypto::decrypt($ciphertext_b64); | |
| 19 | # MODS::Crypto::is_ready(); # 1 if master key + openssl OK | |
| 20 | # MODS::Crypto::ensure_master(); # generate master if missing | |
| 21 | # | |
| 22 | # Master secret lives at /etc/drift_sense/master.key (root:root, mode 600). | |
| 23 | # If missing, encrypt/decrypt refuse to run (fail-safe -- never silently | |
| 24 | # store secrets under a default key). | |
| 25 | #====================================================================== | |
| 26 | use strict; | |
| 27 | use warnings; | |
| 28 | use IPC::Open3; | |
| 29 | use Symbol 'gensym'; | |
| 30 | use POSIX (); | |
| 31 | ||
| 32 | my $OPENSSL_BIN = '/usr/bin/openssl'; | |
| 33 | my $MASTER_KEY_PATH_DFT = '/etc/drift_sense/master.key'; | |
| 34 | my $CIPHER = 'aes-256-cbc'; | |
| 35 | my $KDF_ITERS = 100_000; | |
| 36 | ||
| 37 | #--------------------------------------------------------------------- | |
| 38 | sub is_ready { | |
| 39 | my ($path) = @_; | |
| 40 | return 0 unless -x $OPENSSL_BIN; | |
| 41 | return -r ($path || $MASTER_KEY_PATH_DFT) ? 1 : 0; | |
| 42 | } | |
| 43 | ||
| 44 | #--------------------------------------------------------------------- | |
| 45 | sub ensure_master { | |
| 46 | my ($path) = @_; | |
| 47 | $path ||= $MASTER_KEY_PATH_DFT; | |
| 48 | return 0 if -f $path; | |
| 49 | open(my $u, '<:raw', '/dev/urandom') or die "MODS::Crypto: no /dev/urandom: $!\n"; | |
| 50 | read($u, my $bytes, 32); | |
| 51 | close $u; | |
| 52 | open(my $out, '>', $path) or die "MODS::Crypto: cannot write $path: $!\n"; | |
| 53 | print $out unpack('H*', $bytes); | |
| 54 | close $out; | |
| 55 | chmod 0600, $path; | |
| 56 | return 1; | |
| 57 | } | |
| 58 | ||
| 59 | #--------------------------------------------------------------------- | |
| 60 | sub encrypt { | |
| 61 | my ($plain, %opts) = @_; | |
| 62 | return undef unless defined $plain && length $plain; | |
| 63 | my $master = _load_master($opts{master_key_path}); | |
| 64 | die "MODS::Crypto: master key not available\n" unless defined $master; | |
| 65 | ||
| 66 | my @argv = ($OPENSSL_BIN, 'enc', | |
| 67 | '-' . $CIPHER, | |
| 68 | '-salt', '-pbkdf2', '-iter', $KDF_ITERS, | |
| 69 | '-base64', '-A'); | |
| 70 | return _run($plain, $master, \@argv); | |
| 71 | } | |
| 72 | ||
| 73 | #--------------------------------------------------------------------- | |
| 74 | sub decrypt { | |
| 75 | my ($cipher, %opts) = @_; | |
| 76 | return undef unless defined $cipher && length $cipher; | |
| 77 | my $master = _load_master($opts{master_key_path}); | |
| 78 | die "MODS::Crypto: master key not available\n" unless defined $master; | |
| 79 | ||
| 80 | my @argv = ($OPENSSL_BIN, 'enc', '-d', | |
| 81 | '-' . $CIPHER, | |
| 82 | '-salt', '-pbkdf2', '-iter', $KDF_ITERS, | |
| 83 | '-base64', '-A'); | |
| 84 | return _run($cipher, $master, \@argv); | |
| 85 | } | |
| 86 | ||
| 87 | #--------------------------------------------------------------------- | |
| 88 | sub _load_master { | |
| 89 | my ($override_path) = @_; | |
| 90 | my $path = $override_path || $MASTER_KEY_PATH_DFT; | |
| 91 | return undef unless -r $path; | |
| 92 | open(my $fh, '<', $path) or return undef; | |
| 93 | local $/; my $m = <$fh> // ''; | |
| 94 | close $fh; | |
| 95 | $m =~ s/\s+$//; | |
| 96 | return length $m ? $m : undef; | |
| 97 | } | |
| 98 | ||
| 99 | #--------------------------------------------------------------------- | |
| 100 | # _run(stdin_data, secret, argv) -> stdout on success, undef on fail | |
| 101 | # | |
| 102 | # Writes the master secret to a mode-0600 temp file, passes -pass file:PATH | |
| 103 | # to openssl (safe from ps + env snooping), pipes stdin_data via stdin, | |
| 104 | # captures stdout, cleans up. | |
| 105 | #--------------------------------------------------------------------- | |
| 106 | sub _run { | |
| 107 | my ($stdin_data, $secret, $argv) = @_; | |
| 108 | ||
| 109 | my $key_path = _write_temp_secret($secret); | |
| 110 | my @full_argv = (@$argv, '-pass', "file:$key_path"); | |
| 111 | ||
| 112 | my ($wtr, $rdr, $err_fh); | |
| 113 | $err_fh = gensym; | |
| 114 | my $pid = eval { open3($wtr, $rdr, $err_fh, @full_argv) }; | |
| 115 | if ($@ || !$pid) { | |
| 116 | _unlink_temp($key_path); | |
| 117 | return undef; | |
| 118 | } | |
| 119 | binmode $wtr; | |
| 120 | print {$wtr} $stdin_data; | |
| 121 | close $wtr; | |
| 122 | ||
| 123 | my $out = do { local $/; <$rdr> // '' }; | |
| 124 | my $err = do { local $/; <$err_fh> // '' }; | |
| 125 | close $rdr; close $err_fh; | |
| 126 | waitpid $pid, 0; | |
| 127 | my $rc = $? >> 8; | |
| 128 | _unlink_temp($key_path); | |
| 129 | ||
| 130 | return undef if $rc != 0; | |
| 131 | chomp $out; | |
| 132 | return $out; | |
| 133 | } | |
| 134 | ||
| 135 | sub _write_temp_secret { | |
| 136 | my ($secret) = @_; | |
| 137 | my $t = POSIX::strftime('%s', localtime); | |
| 138 | my $path = "/tmp/.ds_key_${t}_$$_" . int(rand(1_000_000)); | |
| 139 | # Create with tight perms atomically -- O_CREAT|O_EXCL|O_WRONLY, mode 600 | |
| 140 | require Fcntl; | |
| 141 | sysopen(my $fh, $path, Fcntl::O_WRONLY() | Fcntl::O_CREAT() | Fcntl::O_EXCL(), 0600) | |
| 142 | or die "MODS::Crypto: temp $path: $!\n"; | |
| 143 | print $fh $secret; | |
| 144 | close $fh; | |
| 145 | return $path; | |
| 146 | } | |
| 147 | sub _unlink_temp { my $p = shift; unlink $p if $p && $p =~ m!^/tmp/\.ds_key_!; } | |
| 148 | ||
| 149 | 1; |