Vault.pm 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. package Trog::Vault;
  2. use 5.040;
  3. use re '/aasx';
  4. use warnings FATAL => 'all';
  5. use Crypt::AuthEnc::GCM ();
  6. use Crypt::KeyDerivation ();
  7. use Crypt::Misc ();
  8. use Crypt::PRNG ();
  9. use Digest::SHA ();
  10. use File::Slurper ();
  11. use Trog::Auth;
  12. use Trog::Log qw{WARN INFO};
  13. use Trog::SQLite;
  14. =head1 Trog::Vault
  15. Things a user asked us to remember, kept where the database alone will not give
  16. them up.
  17. =head1 WHAT THIS IS FOR
  18. Some of what tCMS does on a user's behalf needs a password that is not tCMS's:
  19. the KeePass passphrase every provisioning recipe's secret: values are behind,
  20. for one. Asking for it every time works, and is what reprovisioning used to do,
  21. but it means the person has to be holding it -- so it gets written down, and the
  22. thing that was too dangerous to keep in a config file ends up in a text file
  23. instead.
  24. So we keep it, and ask for a TOTP code instead of the password when it is time
  25. to use it.
  26. =head1 WHAT A TOTP CODE IS AND IS NOT
  27. It is not a key. Six digits is twenty bits, it changes every thirty seconds,
  28. and we have to know the secret to check it, so nothing can be encrypted with it
  29. and stay encrypted. What it is, is evidence that the person is at the keyboard
  30. right now, which is exactly what you want to demand before handing a stored
  31. password to a process. See Trog::Auth::spend_totp: a code is worth one thing
  32. rather than everything you can do inside its window.
  33. The key is elsewhere.
  34. =head1 WHERE THE KEY IS
  35. An installation has one master key, and it is not in the database, so the
  36. database on its own is ciphertext. It is looked for in this order:
  37. TPSGI_VAULT_KEY base64, in the environment
  38. $CREDENTIALS_DIRECTORY/tpsgi-vault
  39. config/secrets.key base64, in a file
  40. The first is how a real deployment does it, and the reason it is first is the
  41. chroot: tPSGI runs the workers chrooted into the installation, and systemd's
  42. credential store is a ramdisk at /run/credentials which is outside it. So the
  43. unit takes the credential, service/tpsgi.sh reads it before the chroot happens
  44. and puts it in the environment, and the key never touches a disk anywhere.
  45. It is named for tPSGI rather than for tCMS because tPSGI is what owns the unit
  46. and runs whatever is in the directory, and tCMS is only one of the things that
  47. can be. One service, one key.
  48. It is taken out of the environment at load, so nothing we fork afterwards
  49. inherits it -- which matters, because one of the things we fork is a provisioner
  50. that we are handing a decrypted password to on its stdin. Read once, in the
  51. parent, before there are any workers; the workers inherit the key as a variable
  52. and an environment without it.
  53. The second is for a service that is not chrooted.
  54. The third is for a tCMS nothing is handing a key to, which is what you get from
  55. running tPSGI by hand -- the ordinary way to develop against this, since it is
  56. easier to watch and to restart. There is no unit and no service/tpsgi.sh in that
  57. path, so nothing put a key in the environment, and bin/tcms-vault-key --file
  58. writes one here instead.
  59. It is the weakest of the three and the only one you should not be running in
  60. production: a file next to the database it protects is a file that goes into the
  61. same backup.
  62. No key means no vault. Nothing is stored and nothing is offered, and the parts
  63. of tCMS which would have used one ask for the password directly, as they did
  64. before. That is a feature being off, not a failure.
  65. =head1 WHAT IS ACTUALLY DONE TO A SECRET
  66. AES-256-GCM, under a key derived for that one row:
  67. salt 32 random bytes, fresh for every write
  68. key, iv HKDF-SHA256(master, salt) -- 44 bytes, split 32 and 12
  69. aad the username and the secret's name
  70. The salt being fresh per write is what makes the derived key and iv fresh per
  71. write, which is the thing GCM cares about. The username and name go in as
  72. associated data rather than being merely stored alongside, so a row cannot be
  73. lifted into another user's account or renamed into being the answer to a
  74. different question -- the tag stops checking out.
  75. key_id is a fingerprint of the master key. It is not needed to decrypt
  76. anything; it is there so that a row written under a key you no longer have says
  77. so, rather than presenting as corruption.
  78. =head1 FUNCTIONS
  79. =cut
  80. # 32 raw bytes. Read at load: see WHERE THE KEY IS -- this is the moment the
  81. # environment is scrubbed, and it has to be before anything forks.
  82. our $key_file = 'config/secrets.key';
  83. our $credential = 'tpsgi-vault';
  84. our $key_bytes = 32;
  85. my sub decode ( $raw, $whence ) {
  86. $raw =~ s/\s+//g;
  87. my $key = eval { Crypt::Misc::decode_b64($raw) };
  88. if ( !defined $key || length($key) != $key_bytes ) {
  89. WARN("The vault key in $whence is not $key_bytes base64 encoded bytes; ignoring it");
  90. return undef;
  91. }
  92. return $key;
  93. }
  94. our $master = do {
  95. my $raw = delete $ENV{TPSGI_VAULT_KEY};
  96. $raw ? decode( $raw, 'TPSGI_VAULT_KEY' ) : undef;
  97. };
  98. our $looked_further = 0;
  99. # The two places worth looking that are not the environment. Deferred rather
  100. # than done at load, since both are files and this module is loaded by things
  101. # which have no interest in a vault.
  102. my sub look_further { ## no critic (ProhibitSingleUseLexicalSubs) -- 21 lines, which would overcomplicate key()
  103. $looked_further = 1;
  104. my $dir = $ENV{CREDENTIALS_DIRECTORY};
  105. if ($dir) {
  106. my $path = "$dir/$credential";
  107. my $raw = eval { File::Slurper::read_binary($path) };
  108. return decode( $raw, $path ) if defined $raw;
  109. }
  110. my $raw = eval { File::Slurper::read_binary($key_file) };
  111. return undef unless defined $raw;
  112. # A key file the group or the world can read is a key file that has already
  113. # been read. Said rather than refused: the alternative is a site that
  114. # silently stops being able to open its own secrets.
  115. my @stat = stat($key_file);
  116. WARN("$key_file is readable by more than its owner; chmod 600 it") if @stat && ( $stat[2] & oct('077') );
  117. return decode( $raw, $key_file );
  118. }
  119. =head2 key() = STRING or undef
  120. The master key, or undef when this installation has none.
  121. =head2 has_key() = BOOL
  122. Whether there is a vault at all. Ask this before offering a user anywhere to
  123. put a secret, so that a site with no key says so rather than accepting one and
  124. losing it.
  125. =cut
  126. sub key {
  127. $master //= look_further() unless $looked_further;
  128. return $master;
  129. }
  130. sub has_key { return key() ? 1 : 0 }
  131. =head2 key_id() = STRING
  132. A fingerprint of the master key, stored on each row so that a row written under
  133. a key which is no longer here can say which.
  134. =cut
  135. sub key_id {
  136. my $key = key() or return '';
  137. return substr( Digest::SHA::sha256_hex($key), 0, 16 );
  138. }
  139. # Bound into the ciphertext, so that moving a row between users or renaming it
  140. # breaks the tag rather than working.
  141. my sub aad ( $user, $name ) { return join( "\0", $user, $name ) }
  142. my sub derive ($salt) {
  143. my $material = Crypt::KeyDerivation::hkdf( key(), $salt, 'SHA256', 44, 'tcms/user-secret' );
  144. return ( substr( $material, 0, 32 ), substr( $material, 32, 12 ) );
  145. }
  146. # The same database the users are in, since a secret belongs to one.
  147. my sub dbh { return Trog::SQLite::dbh( 'schema/auth.schema', 'config/auth.db' ) }
  148. =head2 set($user, $name, $secret) = ($ok, $message)
  149. Remember one thing for one user, replacing whatever was there under that name.
  150. =cut
  151. sub set ( $user, $name, $secret ) {
  152. return ( 0, 'this installation has no vault key, so there is nowhere to keep it' ) unless has_key();
  153. # A name is a label in a form and a key in a table, and it is half of what
  154. # binds a row to its owner, so it is a short plain thing or it is nothing.
  155. return ( 0, 'a secret needs a name' ) if !$name || length $name > 64 || $name !~ m/^[[:alnum:]][\w.-]*$/;
  156. return ( 0, 'nothing to store' ) if ( $secret // q{} ) eq q{};
  157. my $salt = Crypt::PRNG::random_bytes(32);
  158. my ( $key, $iv ) = derive($salt);
  159. my $gcm = Crypt::AuthEnc::GCM->new( 'AES', $key, $iv );
  160. $gcm->adata_add( aad( $user, $name ) );
  161. my $ciphertext = $gcm->encrypt_add($secret);
  162. my $tag = $gcm->encrypt_done();
  163. my $dbh = dbh();
  164. eval {
  165. $dbh->do(
  166. "INSERT OR REPLACE INTO user_secret (username, name, key_id, salt, ciphertext, tag, created) VALUES (?,?,?,?,?,?,?)",
  167. undef, $user, $name, key_id(), $salt, $ciphertext, $tag, time()
  168. );
  169. 1;
  170. } or do {
  171. WARN("Could not store the secret '$name' of $user: $@");
  172. return ( 0, 'could not store it' );
  173. };
  174. Trog::Auth::log_event( 'secret_write', $user );
  175. return ( 1, "'$name' stored" );
  176. }
  177. # The plaintext, or undef with a reason in the log. Never dies: a row that will
  178. # not open is a thing to report to somebody who can store it again, not an
  179. # exception halfway through drawing a page.
  180. my sub _open ( $row, $user, $name ) {
  181. if ( $row->{key_id} ne key_id() ) {
  182. WARN( "'$name' for $user was sealed under vault key $row->{key_id}, and this installation has " . key_id() );
  183. return undef;
  184. }
  185. my ( $key, $iv ) = derive( $row->{salt} );
  186. my $gcm = Crypt::AuthEnc::GCM->new( 'AES', $key, $iv );
  187. $gcm->adata_add( aad( $user, $name ) );
  188. my $secret = $gcm->decrypt_add( $row->{ciphertext} );
  189. # The tag is the whole point: it says this ciphertext, under this key, for
  190. # this user and this name, is the one that was written.
  191. if ( !$gcm->decrypt_done( $row->{tag} ) ) {
  192. WARN("'$name' for $user did not survive its own tag check; it has been tampered with or the database is damaged");
  193. return undef;
  194. }
  195. return $secret;
  196. }
  197. # What is in one row, or nothing.
  198. #
  199. # Every question anybody asks about a stored secret comes through here, get()
  200. # and has() alike, and they all get the same answer -- which is the point. A
  201. # page that says a secret is there and a request that finds it will not open are
  202. # a way to send somebody round a loop typing codes at a row that was never going
  203. # to work. Opening it is the only way to know, and it is microseconds.
  204. #
  205. # What get() adds is that reading a secret is an event: it moves last_used and
  206. # writes to the audit log. Asking whether one is usable is not an event, and
  207. # does not.
  208. my sub stored ( $user, $name ) {
  209. return undef unless has_key();
  210. my $dbh = dbh();
  211. my $rows = $dbh->selectall_arrayref(
  212. "SELECT key_id, salt, ciphertext, tag FROM user_secret WHERE username=? AND name=?",
  213. { Slice => {} }, $user, $name
  214. );
  215. return undef unless ref $rows eq 'ARRAY' && @$rows;
  216. return _open( $rows->[0], $user, $name );
  217. }
  218. =head2 get($user, $name) = STRING or undef
  219. What was stored, or undef if there is nothing under that name, no key to open it
  220. with, or a key which is not the one it was sealed under.
  221. Every call is an audit_log row. Reading a stored password is the interesting
  222. event here, not writing one.
  223. =cut
  224. sub get ( $user, $name ) {
  225. my $secret = stored( $user, $name );
  226. return undef unless defined $secret;
  227. my $dbh = dbh();
  228. $dbh->do( "UPDATE user_secret SET last_used=? WHERE username=? AND name=?", undef, time(), $user, $name );
  229. Trog::Auth::log_event( 'secret_read', $user );
  230. return $secret;
  231. }
  232. =head2 list($user) = ARRAYREF
  233. What this user has stored, as names and dates. Never values -- there is no
  234. route, page or log line anywhere that shows one back to them, because a secret
  235. you can read off a screen is one you did not need us to keep.
  236. =cut
  237. sub list ($user) {
  238. my $dbh = dbh();
  239. my $rows = $dbh->selectall_arrayref(
  240. "SELECT name, created, last_used, key_id, salt, ciphertext, tag FROM user_secret WHERE username=? ORDER BY name",
  241. { Slice => {} }, $user
  242. );
  243. return [] unless ref $rows eq 'ARRAY';
  244. # Opened rather than guessed at from the key fingerprint, so that a row this
  245. # page calls readable is one that will actually open when something asks for
  246. # it. This is the page somebody comes to when a secret has stopped working,
  247. # and it is no use to them if it agrees that everything looks fine.
  248. foreach my $row (@$rows) {
  249. $row->{readable} = ( has_key() && defined _open( $row, $user, $row->{name} ) ) ? 1 : 0;
  250. delete $row->{$_} foreach qw{salt ciphertext tag};
  251. }
  252. return $rows;
  253. }
  254. =head2 has($user, $name) = BOOL
  255. Whether there is something stored under that name that this installation can
  256. actually open.
  257. Not "is there a row": a row sealed under a key that is gone is a row that will
  258. never open again, and answering yes to this would have somebody typing codes at
  259. it. The question this is really being asked is whether to draw a passphrase box
  260. or a code box, and the honest answer to that is the one where the code works.
  261. Separate from get() because reading a secret is an event and asking about one is
  262. not. This moves nothing and writes nothing to the audit log.
  263. =cut
  264. sub has ( $user, $name ) { return defined stored( $user, $name ) ? 1 : 0 }
  265. =head2 forget($user, $name) = BOOL
  266. =cut
  267. sub forget ( $user, $name ) {
  268. my $dbh = dbh();
  269. my $res = $dbh->do( "DELETE FROM user_secret WHERE username=? AND name=?", undef, $user, $name );
  270. Trog::Auth::log_event( 'secret_delete', $user ) if $res && $res != 0;
  271. return $res && $res != 0 ? 1 : 0;
  272. }
  273. 1;