Dancer2-Session-Pg

 view release on metacpan or  search on metacpan

lib/Dancer2/Session/Pg/Cipher.pm  view on Meta::CPAN

=encoding utf8

=for stopwords AEAD AES ChaCha DDL DSN GCM Kubernetes NIST OpenID Poly XHR crashloops dbh dbpass dbschema dbtable dbuser decrypt decryptable decrypted decrypts deserialise deserialising diagnosable dsn encryptions nonces plaintext preforked rollout s...

=head1 NAME

Dancer2::Session::Pg::Cipher - the authenticated-cipher contract for Dancer2::Session::Pg

=head1 VERSION

version 0.001

=head1 SYNOPSIS

    package My::Cipher::XChaCha20;

    use Moo;
    with 'Dancer2::Session::Pg::Cipher';

    use Crypt::AuthEnc::ChaCha20Poly1305 ();

    sub cipher_id   { return 200 }            # third-party range: 128..255
    sub cipher_name { return 'XChaCha20-Poly1305' }
    sub key_bytes   { return 32 }
    sub iv_bytes    { return 24 }             # a 24-byte nonce, unlike the core four
    sub tag_bytes   { return 16 }

    # $aad is authenticated but NOT encrypted, and must not be dropped: it is
    # what binds a sealed payload to the session id it belongs to.
    sub seal {
        my ( $self, $key, $iv, $plaintext, $aad ) = @_;
        return My::XChaCha::seal( $key, $iv, $aad, $plaintext );    # ( $ciphertext, $tag )
    }

    sub unseal {
        my ( $self, $key, $iv, $ciphertext, $tag, $aad ) = @_;
        return My::XChaCha::open( $key, $iv, $aad, $ciphertext, $tag );  # $plaintext or undef
    }

    # and then, in the application, as the alg of a slot
    #   encryption_keys:
    #     0: { key: "...", alg: "AES-256-GCM" }          # kept, read only
    #     1:
    #       key:    "..."
    #       alg:    "My::Cipher::XChaCha20"
    #       active: true

=head1 DESCRIPTION

L<Dancer2::Session::Pg> stores a cipher identifier in every payload it writes, so
the cipher a session was written with is a property of the row rather than of the
configuration. That is what makes a cipher replaceable: point C<alg> at a new one
and new sessions use it while existing rows stay readable until they expire.

This role is the contract such a cipher implements. Consume it with C<with>
rather than duck-typing the methods: that is how L</cipher_self_check> arrives,
and the engine refuses a cipher that does not provide it.

=head2 Only authenticated modes

A session frequently carries the credentials that prove who somebody is. A row
that has been altered in the database must B<fail to decrypt>, not deserialise
into a structure the application then trusts, so every cipher here produces a
tag and verifies it. L</cipher_self_check> tests exactly that, by flipping a bit
and insisting the result is refused.

=head2 Cipher ids are permanent

The id is one byte in every stored payload. Changing a cipher's id makes rows
written under the old one unreadable, which logs those users out.

    1 .. 127      reserved for ciphers shipped with Dancer2::Session::Pg
    128 .. 255    third-party range

The engine croaks at construction if two readable ciphers claim one id, so a
collision is a startup failure rather than a payload that decrypts as the wrong
thing. Within the third-party range a deployment owns its own collisions.

=head2 Your key length is your own business

A cipher declares the C<key_bytes> it needs, and the engine checks the key of the
slot your cipher sits in against B<that cipher and nothing else>. Other slots may
hold keys of other lengths; it does not concern you.

So there is no equal-key-length restriction to design around. A deployment can
rotate from a 16-byte cipher to a 32-byte one in a single step by giving the new
pair a slot of its own, which is what L<Dancer2::Session::Pg/Changing the key
length> describes and what the distribution's own test suite exercises.

What you must not do is assume anything about the key beyond its length. One
engine may hold several keys, a given row names the slot that sealed it, and your
C<seal> and C<unseal> are handed the key for that slot and never asked to choose.

=head1 REQUIRED METHODS

=head2 cipher_id

An integer in C<1 .. 255>, written into every payload. Permanent; see above.

B<Four are already taken>, and they are part of the stored format, so they are
not available for reuse:

    1    AES-128-GCM            (Dancer2::Session::Pg::Cipher::AESGCM, 16-byte key)
    2    AES-192-GCM            (the same class, 24-byte key)
    3    AES-256-GCM            (the same class, 32-byte key)
    4    ChaCha20-Poly1305      (Dancer2::Session::Pg::Cipher::ChaCha20Poly1305)

B<Pick from C<128 .. 255> for a cipher of your own.> C<1 .. 127> is reserved
for built-ins, used or not, and L</cipher_self_check> refuses an id in that
range that no built-in has yet taken -- claiming one would work perfectly today
and collide with a future built-in, at which point the rows written under it
become unreadable.

Claiming one of the four ON PURPOSE is a legitimate thing to do -- a
hardware-accelerated, audited or vendored implementation of the same algorithm
has to keep the id, or it could not read the rows it is replacing. But it means
exactly one thing, B<this class is a byte-compatible drop-in for that cipher>,
and L</cipher_self_check> B<enforces it> rather than trusting it: a class
claiming a reserved id must read a payload the built-in wrote, and the built-in
must read one it wrote. Both directions, because both happen -- the engine reads
old rows after the swap, and the built-in reads the replacement's rows if it is



( run in 1.319 second using v1.01-cache-2.11-cpan-3f962061493 )