NAME
Crypt::MultiKey::Vault - Encrypted block storage that can be unlocked with various combinations of keys
DESCRIPTION
Vault is similar to Coffer, but instead of managing a single secret in memory, it encrypts and decrypts random-access sectors of the source file. The initial few sectors of the file are plain text and store the details of the key-wrapping implementation, followed by sectors of encrypted binary data. The sector encryption algorithm is compatible with Linux crypt-dm, allowing you to map the Vault as a Linux block device with transparent encryption, if you want to.
CONSTRUCTORS
new
$vault= Crypt::MultiKey::Vault->new(%attributes);
This creates a new uninitialized Vault. Any configuration or data writes to this object will be cached in memory until you call "save".
load
$vault= Crypt::MultiKey::Vault->load($path, %attributes);
$vault= Crypt::MultiKey::Vault->load($handle, %attributes);
$vault= Crypt::MultiKey::Vault->load(%attributes); # with 'handle' or 'path'
This loads an existing Vault file and unpacks its metadata. The data cannot be read until you call "unlock".
ATTRIBUTES
path
The path to the Vault file. This can only be changed by a call to "save".
handle
An open seekable file handle to the Vault's file. This will be undef if and only if the Vault has not yet been saved.
cipher
Currently only AES-256-XTS is supported. This can be changed during "save" if you are saving to a new file.
sector_size
The block size used for encrypting the file. It must be a power of 2, ideally between 512 and 4096 (for compatibility with Linux dm-crypt). This can be changed during "save" if you are saving to a new file.
header_offset
The byte offset at which the Vault header begins. Data before this offset is the "preamble". This is updated automatically if you specify a new preamble during "save".
file_preamble
A user-defined text to write at the beginning of the Vault file. The text may not include the string "\0===== Crypt::MultiKey::Vault =====\n". The feature is provided to allow the Vault to be prefixed with a script. This can only be changed during a call to "save".
data_offset
The byte offset at which data blocks begin. This must be a multiple of the sector size, and by default will not be smaller than 64KiB to leave room for the header to grow without needing to rewrite the file. This can be changed during "save" if you are saving to a new file.
data_size
The length in bytes of the data area. e.g. file size minus data_offset. This can be changed at any time via "resize". The attribute is read-only for safety.
bundled_keys
$self->bundled_keys(1) # serialize protected PKeys in header
$self->bundled_keys('public') # serialize OpenSSL 'PUBLIC KEY' PEM in header
If set to a true value, any PKey object referenced by the "lock_mechanism" with a protection_scheme defined (i.e. encrypted) will be serialized into the Vault header during "save". If set to the value 'public', only the public half of each PKey will be serialized, in OpenSSL PUBLIC KEY format and lacking any PKey metadata.
This feature exists to help keep the PKey objects tightly associated with the Vault file so that all you need to open the Vault are the credentials for the PKey protection schemes. This attribute must be specified in the constructor (or "save" in save method), and will not be preserved across save/load of the Vault. This gives the caller control over whether PKey objects are trusted from this Vault file.
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 Vault has been initialized with locks but the primary secret key is not currently available. A new, uninitialized Vault is not considered locked.
user_meta
An arbitrary hashref of JSON-compatible metadata that will be added to the Vault header. Note that headers are plaintext. If you wish to store secret metadata, write it into the encrypted data area rather than into the plaintext header.
Warning: the authenticity of user_meta does not get checked until you have unlocked the Vault. Never trust user_meta on a locked Vault 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".
METHODS
interactive_unlock
$bool= $vault->interactive_unlock(%options);
Shortcut for
Crypt::MultiKey::InteractiveUnlock
->new(target => $vault, %options)
>run;
lock
Delete the "primary_skey" attribute from the "lock_mechanism", and any attributes holding unencrypted secrets.
save
$vault->save(%options);
#options:
# path - path and file name to write
# sector_size - power of 2 between 512 and 4096
# data_offset - file offset which must point to a sector boundary
# user_meta - arbitrary JSON-compatible perl data
# name - shortcut for user_meta->{name}
# cipher - currently must be 'AES-256-XTS'
# file_preamble - a string to start the file, such as a '#!' line
# bundled_keys - whether to include encrypted/public PKey inside the Vault
Write out a new Vault file, or write changes to the metadata of an existing file. If you request changes to data_offset, sector_size, or cipher (or enlarge user_meta beyond what fits before data_offset) this will trigger a complete rewrite of the file. A complete rewrite requires you to supply a path for the new file and it must not already exist.
resize
Extend or truncate the data region of the file. This is just a call to 'truncate' on the file handle, so it does not need to reconstruct the file.
read
$secret= $vault->read($ofs, $size);
Read (and decrypt) a region of the data area. This does not need to be block-aligned, but is more efficient when aligned. Note that there is no integrity check for data blocks. If the requested range extends past the end of the data area, the read will be truncated. Requesting an offset beyond the end of the data area croaks.
write
$vault->write($ofs, $secret_or_span);
Write plaintext to a region of the data area. This does not need to be block-aligned, but is more efficient when aligned. The file will be enlarged if you write beyond the end of the file.
create_block_device
$vault->create_block_device(name => $mapper_dev_name);
This currently only works on Linux, and probably requires root access. If the "handle" is not a block device (likely) this starts by creating one with losetup(1). It then calls dmsetup(1) to create a new mapper device using dm-crypt, and then releases the loop device so that it gets deleted automatically once the mapper device is closed. You can then directly perform reads and writes on the data area of this file by reading and writing that block device, and at the speed of the kernel.
Dies on failure. It also attempts to clean up the loopback and/or dm device on failure.
Patches welcome for supporting other operating systems.
FILE FORMAT
A Vault is defined in terms of a sector_size. The header occupies an integer number of sectors, but also ensures that the data starts on a 4KiB boundary. The header has an optional user-defined "preamble" (useful for fun things like a shebang that makes the vault executable) followed by a format marker, followed by a version declaration, followed by a JSON object holding the rest of the header data. The JSON can optionally be followed by serialized PKey objects (having encrypted private halves) if you wish to keep your keys close to the thing they unlock. The remainder of the header is padded with \n bytes, up to the last 32 bytes which hold an HMAC-SHA256 of all bytes in the header up to the HMAC.
The purpose of this format is to allow you to see what is inside it using a text editor, easily parse it with other tools if you need to, and have all the binary data pushed off the bottom of the screen by the run of "\n" characters to reduce the chance that you corrupt your terminal. The run of "\n" characters also provide padding so that the header can be rewritten without needing to replace the entire file. I departed from PEM encoding because the primary purpose of PEM is to be ascii-safe during transmission, but a Vault will always need full 8-bit capability, and JSON is easier to store user metadata without PEM header restrictions.
$optional_preamble
\0
===== Crypt::MultiKey::Vault =====
version: 0.001
{
"cipher": "AES-256-XTS",
"sector_size": 4096,
"data_sector": 128,
"user_meta": {
"name": "Example"
},
"locks": [
{ "cipher": "AES-256-GCM",
"ciphertext": $base64,
"tumblers": [
{ "ephemeral_pubkey": $base64, "key_fingerprint": "SHA256:base64==" }
]
},
{ "cipher": "AES-256-GCM",
"ciphertext": $base64,
"tumblers": [
{ "ephemeral_pubkey": $base64, "key_fingerprint": "SHA256:base64==" },
{ "ephemeral_pubkey": $base64, "key_fingerprint": "SHA256:base64==" }
]
}
],
"writer": "Crypt::MultiKey::Vault 0.001"
}
\0
[optional bundled PKey objects in PEM format]
\n
\n (repeating until 32 bytes before data_sector)
$HMAC_BYTES
[binary data begins at data_sector * sector_size]
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.