Diff -- /var/www/vhosts/3dshawn.com/site1/MODS/Crypto.pm

O Operator
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
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
1package 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#======================================================================
26use strict;
27use warnings;
28use IPC::Open3;
29use Symbol 'gensym';
30use POSIX ();
31
32my $OPENSSL_BIN = '/usr/bin/openssl';
33my $MASTER_KEY_PATH_DFT = '/etc/drift_sense/master.key';
34my $CIPHER = 'aes-256-cbc';
35my $KDF_ITERS = 100_000;
36
37#---------------------------------------------------------------------
38sub 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#---------------------------------------------------------------------
45sub 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#---------------------------------------------------------------------
60sub 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#---------------------------------------------------------------------
74sub 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#---------------------------------------------------------------------
88sub _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#---------------------------------------------------------------------
106sub _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
135sub _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}
147sub _unlink_temp { my $p = shift; unlink $p if $p && $p =~ m!^/tmp/\.ds_key_!; }
148
1491;