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 "content_dict".
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.
- is_dict
-
Accessor to test whether the content_type indicates a dictionary encoding.
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
contentorcontent_dictattributes 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_dict
This attribute is used when the content type is application/crypt-multikey-coffer-dict. and allows you to work with a hash of name/value pairs where each value is a SecretBuffer or Span. Changes to this hash will not be seen automatically; either use "set", or write the whole attribute to properly indicate to the Coffer that it needs re-encrypted.
content_changed
True if you have used accessors to alter your content or content_dict attribute. 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 content_type is application/crypt-multikey-coffer-dict, this method can be used to retrieve a secret by name. If the content is not yet decrypted, it will try decrypting it and fail if the Coffer is "locked".
set
$coffer->set($name, $secret);
When content_type is application/crypt-multikey-coffer-dict, this method can be used to store a secret by name. If the content is not yet decrypted, it will try decrypting it and fail if the Coffer is "locked". Using this method when no content or ciphertext are defined will initialize the content_type to application/crypt-multikey-coffer-dict and the content_dict attribute to a hashref.
If the $secret is not defined, it deletes $name from the hashref. There is no way to store a state of "exists but undefined".
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 content_dict) attribute. The content or content_dict attributes 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 or content_dict 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_001
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.