NAME
Crypt::MultiKey::Coffer - Encrypted container that can be unlocked with various combinations of keys
SYNOPSIS
# Coffer is locked/unlocked using public/private keys
my ($key1, $key2, $key3)= map Crypt::MultiKey::PKey->generate('x25519'), 1..3;
# initial state of coffer is not locked, and unsaved
my $coffer= Crypt::MultiKey::Coffer->new(
path => './mydata.coffer',
content => $secret_buffer,
);
$coffer->add_access($key1); # now coffer can be unlocked by key1
$coffer->add_access($key2, $key3); # now coffer can be unlocked by key2+key3
$coffer->save; # write encrypted PEM to file 'mydata.coffer'
$coffer->lock; # now coffer cannot be read until unlocked
$coffer->unlock($key2,$key3); # content decrypted from ciphertext
$secret= $coffer->content; # access secret data
# Coffer can be used in key/value mode.
# (multiple named secrets get concatenated and encrypted as one secret)
my $coffer= Crypt::MultiKey::Coffer->new(
path => './mydata.coffer',
);
$coffer->set("secret1", $secret1);
$coffer->set("secret2", $secret2);
$coffer->add_access($key2);
$coffer->save;
$coffer->lock;
$coffer->unlock($key2);
$secret2= $coffer->get("secret2");
DESCRIPTION
CONSTRUCTORS
new
$coffer= Crypt::MultiKey::Coffer->new(%attributes);
Construct a new Coffer. The attributes are applied to the object as method calls.
load
# as a constructor
$coffer= Crypt::MultiKey::Coffer->load($source, %options);
# as a method
$coffer->load($source, %options);
# $source may be:
# $file_path
# \$buffer
# Crypt::SecretBuffer
# Crypt::SecretBuffer::Span
# Crypt::SecretBuffer::PEM
# options:
# path => $file_path # value for 'path' attribute when using a buffer
# bundled_keys => $bool # whether to process PKey PEM blocks found in buffer
Load a Coffer from a file or buffer or PEM object. This does not decrypt the data. See "unlock".
When loading from a file or buffer, the PEM encoding of the Coffer may be followed by PEM encodings of the PKey objects. If you request bundled_keys, they will be inflated to PKey objects and passed to "insert_keys". This will also initialize the "bundled_keys" attribute of the created object.
Neither 'path' nor 'bundled_keys' attributes are serialized in the Coffer PEM, for security reasons. They must be specified / requested by the caller.
ATTRIBUTES
path
Filesystem path from which to load and save the Coffer.
bundled_keys
$coffer->bundled_keys(1); # $coffer->save will also export referenced PKeys
$coffer->bundled_keys('public'); # coffer PEM will have OpenSSL 'PUBLIC KEY' PEM appended
The "locks" attribute references the keys that can unlock it by the key fingerprint. The keys can be saved separately to be loaded by the application and added with "insert_keys", or they can be appended to the Coffer to be loaded automatically when loading the Coffer, to keep everything together in one place. When this option is set to a true value, the "export" method will write out the Coffer PEM block followed by the PEM serializations of each of the PKey objects. (They serialize as either public-only or encrypted-private PEM blocks with PKey metadata included. Obviously it would defeat the purpose of the Coffer to serialize unencrypted private keys to the same file)
You can set this option to 'public' to write only the public key in the standard OpenSSL format without any of the PKey metadata. Having the full public key present allows a new Coffer to be written that is decryptable by all the same PKeys as the current one, while not giving any advantage to an attacker by showing them the PKey metadata.
lock_mechanism
This object handles the details of locking and unlocking, and lets Coffer and Vault share code. The implementation could be configurable in the future, but currently only the default of Crypt::MultiKey::LockMechanism is supported.
Several methods are directly delegated to this object:
locked
True if the Coffer has been initialized with locks but the primary secret key is not currently available. A new, uninitialized Coffer is not considered locked.
user_meta
An arbitrary hashref of name/value strings that will be added to the exported PEM as headers of the form user_meta.$name = $value. Note that headers are plaintext. If you wish to store secret user metadata it needs to be part of "content", which can be accomplished conveniently using "get" and "set".
Because PEM has no escaping system, the names and values may not contain control characters or begin or end with space characters. The names also may not contain '.' or be purely numeric, because these are used for encoding the structure of the data.
Warning: the authenticity of user_meta does not get checked until you have unlocked the coffer. Never trust user_meta on a locked Coffer unless the file was stored securely.
name
A shortcut for ->user_meta->{name}. This helps encourage you to at least provide a label for the file indicating its purpose or contents. This defaults to the basename of the "path".
cipher_data
If the coffer has been encrypted/locked, this attribute holds a hashref including the ciphertext and other parameters describing how it was encrypted. In a newly-initialized Coffer this will be undef.
- has_ciphertext
-
True if the cipher_data is defined and contains a ciphertext string
content_type
Specify the MIME type of the "content" attribute. The special value application/crypt-multikey-coffer-dict enables the "get" and "set" methods to use the content as a key/value dictionary. This is stored unencrypted in the headers of the Coffer, so you may wish to omit it for non-dictionary types if the content type would leak information.
- is_dict
-
Accessor to test whether the content_type indicates a dictionary encoding. Currently only
application/crypt-multikey-coffer-dictis supported.
content
This attribute is an unencrypted Crypt::SecretBuffer of the secret data of the Coffer. If it isn't initialized and an encrypted copy exists ("has_ciphertext" is true), reading this attribute will attempt to decrypt it, and fail if the Coffer is still locked. If there is no encrypted copy (such as when a Coffer object is first created) reading this attribute just returns undef.
Writing this attribute will invalidate the cipher_data attribute, forcing it to be re-encrypted when you call "save". Beware that if you make changes to the SecretBuffer object directly, the Coffer object will not be aware of those changes and the changes may be lost if the Coffer doesn't know they need re-encrypted. Set $coffer->content_changed(1) if you need to flag the content as having changed.
If you are using the Coffer for name/value dictionary storage, use the "get" and "set" methods instead of accessing this attribute. In dictionary mode, accessing this attribute will trigger a serialization of the data.
- has_content
-
True if the
contentattribute or hidden dictionary storage are defined, meaning that either the Coffer is decrypted or has been initialized to a new value. Maybe unintuitively, it returns false for a not-locked coffer where the content hasn't been lazy-decrypted yet. - initialized
-
True if
has_contentorhas_ciphertext, meaning that content has been added to this Coffer.
content_changed
True if you have used accessors to alter your content or hidden dictionary storage. If you modify the content SecretBuffer yourself, you should set this attribute to true so that the Coffer knows it needs to re-encrypt the content.
authentication
When loaded from an external source (currently just PEM files), this attribute gets initialized to an arrayref of the canonical message (PEM headers) and the HMAC-SHA256 of that text. This will be verified during "unlock" to ensure that the headers were not altered, throwing an exception if they don't match. Beware that until unlocked, you have no guarantee that the headers weren't altered by an attacker. For an example attack, consider what happens if you load a Coffer file, assign new content without unlocking the old content, and then re-encrypt using the same public keys from the previous locks. An attacker could inject a bogus lock using a key they control, and then your re-encrypted Coffer file would be readable by them! Always "unlock" a Coffer before trusting any attribute of the object.
This attribute will be undef if the Coffer was not loaded from an external source. The check during "unlock" is only performed if this attribute is defined.
METHODS
interactive_unlock
$bool= $coffer->interactive_unlock(%options);
Shortcut for
my $iu= Crypt::MultiKey::InteractiveUnlock->new(target => $coffer, %options);
$iu->run;
get
$secret= $coffer->get($name);
When using dictionary mode (based on "content_type" / "is_dict" flag), this method can be used to retrieve a secret by name. It dies if the content_type is not a supported dictionary type. In Coffer's dictionary mode, a name that exists in the collection always has a defined value, so checking defined-ness of the return value is equivalent to checking 'exists' on a perl hashref.
If the content is not yet decrypted, it will try decrypting it and fail if the Coffer is "locked".
Do not modify the buffer referenced by the returned $secret Span object.
set
$coffer->set($name, $secret);
When using dictionary mode (based on "content_type" / "is_dict" flag), this method can be used to store a secret by name. Using this method when no content or ciphertext are defined will initialize the content_type to application/crypt-multikey-coffer-dict and the hidden dictionary storage. If content exists but is not yet decrypted, this will try decrypting it and fail if the Coffer is "locked".
If $secret is not defined, it deletes $name from the dictionary. There is no way to store a state of "exists but undefined".
list_names
@names= $coffer->list_names;
@names= $coffer->list_names($prefix);
When using dictionary mode (based on "content_type" / "is_dict" flag), this method returns a list of the available names. You can optionally filter them by a prefix. The names are returned as Span objects. Do not modify the buffer underlying the name Spans. See also "list_names_plaintext".
list_names_plaintext
Same as "list_names", but returns plain strings instead of Span objects.
export
$buf= $coffer->export;
Serialize the Coffer to a buffer, in PEM format.
save
$coffer->save; # saves to ->path
$coffer->save($path); # save to specific path, and initialize path attribute
Save changes to disk. If you specify the $path and the path attribute is not already set, this initializes it. This writes a new file and then renames it into place to ensure it doesn't corrupt the existing file.
authenticate
$bool= $coffer->authenticate;
$coffer->authenticate(1); # automatic croak
Validate the "authentication" attribute, returning boolean. This can only be called after unlocking the Coffer. Pass a true value to have it croak on failure instead of returning false.
lock
Delete the "primary_skey" attribute from the "lock_mechanism", and any attributes holding unencrypted secrets.
encrypt
$coffer->encrypt;
Regenerate the "cipher_data" attribute from "content" (or hidden dictionary storage). The content attribute or hidden dictionary storage must be initialized. This will use a fresh AES key if all lock tumblers have public keys present.
This is called automatically during "save" if the Coffer is aware that the "cipher_data" is not current.
decrypt
$coffer->decrypt;
Regenerate the content attribute from the cipher_data attribute. Returns $coffer for chaining. The "cipher_data" attribute must be initialized and the correct primary secret key must be loaded in the "lock_mechanism".
This is called automatically when accessing an uninitialized content in either scalar or dictionary mode if the Coffer is not locked.
FILE FORMAT
A Coffer is encoded in PEM format, with headers that describe the contents of the coffer and which keys can unlock it.
-----BEGIN CRYPT MULTIKEY COFFER-----
version: 0.001
writer_version: 0.001
user_meta.name: Example
locks.0.cipher: AES-256-GCM
locks.0.ciphertext: base64==
locks.0.tumblers.0.ephemeral_pubkey: base64==
locks.0.tumblers.0.key_fingerprint: SHA256:base64==
locks.1.cipher: AES-256-GCM
locks.1.ciphertext: base64==
locks.1.tumblers.0.ephemeral_pubkey: base64==
locks.1.tumblers.0.key_fingerprint: SHA256:base64==
locks.1.tumblers.1.ephemeral_pubkey: base64==
locks.1.tumblers.1.key_fingerprint: SHA256:base64==
content_type: text/plain
cipher_data.cipher: AES-256-GCM
pem_header_authentication: HMAC-SHA256:base64==
base64==
-----END CRYPT MULTIKEY COFFER-----
The content is either binary data of your choice, or a key/value format written by this module which is just a series of length-delimited strings. The content is encrypted with AES-256 and written as base64 as the body of the PEM file. The AES-GCM key that encrypted the content is encrypted in one or more "lock" entries and the AES encryption key for each access is derived from the combined key material from the "tumblers". A "tumbler" is a set of parameters that can be combined with the private half of a public/private key to generate AES-key material.
VERSION
version 0.000_002
AUTHOR
Michael Conrad <mike@nrdvana.net>
COPYRIGHT AND LICENSE
This software is copyright (c) 2026 by Michael Conrad.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.