NAME
Crypt::SecretBuffer - Prevent accidentally leaking a string of sensitive data
SYNOPSIS
use Crypt::SecretBuffer 'secret';
$buf= secret;
print "Enter your password: ";
$buf->append_console_line(STDIN) # read TTY with echo disabled
or die "Aborted";
say $buf; # prints "[REDACTED]"
my @cmd= qw( openssl enc -e -aes-256-cbc -md sha512 -pbkdf2 -iter 239823 -pass fd:3 );
IPC::Run::run(\@cmd,
'0<', \$data,
'1>', \$ciphertext,
'3<', $buf->as_pipe # Feed the password to an external command
); # without it ever being copied into a Perl scalar
undef $buf; # no copies of password remain in memory.
DESCRIPTION
This module helps you protect a secret value from getting copied around unintentionally or lingering in memory of a long-running program. It is very much like SecureString from .NET, but with a better name. (preventing accidental copies does not make something "secure", and "string" sometimes implies text or immutability) While a scripting language in general is a poor choice for managing sensitive data in a long-lived app instance, this at least gives you some measure of control over how long secrets remain in memory, and how easy it is to accidentally expose them to other code, such as log messages. When you free a SecretBuffer, you can be fairly sure that the secret does not remain anywhere in your process address space. (with the exception of when it's being fed into a pipe in the background; see "as_pipe")
This module exists because in standard OpenSSL examples they always wipe the buffers before exiting a function, but with Perl's exception behavior (croak
) there was no way to ensure that the buffers got wiped before exiting a function. By putting all the secrets into Crypt::SecretBuffer objects, it at least ensures that the buffers are always wiped according to standard practices for C code. Passing around SecretBuffer objects perl-side is just an added benefit.
The SecretBuffer is a blessed reference, and the buffer itself is stored in XS in a way that the Perl interpreter has no knowledge of. Any time the buffer needs reallocated, a new buffer is allocated, the secret is copied, and the old buffer is wiped clean before freeing it. It also guards against timing attacks by copying all the allocated buffer space instead of just the length that is occupied by the secret.
The API also provides you with a few ways to read or write the secret, since any read/write code implemented directly in Perl would potentially expose your secret to having copies made in temporary buffers. But, for interoperability with other Perl code, you can also toggle whether stringification of the buffer reveals the secret or not. For instance:
say $buf; # stringifies as [REDACTED]
{
local $buf->{stringify_mask}= undef;
some_xs_function($buf); # stringifies as the secret
}
say $buf; # stringifies as [REDACTED]
There is no guarantee that the XS function in that example wouldn't make a copy of your secret, but this at least provides the secret buffer directly to the XS code that calls SvPV
without making a copy. If an XS module is aware of Crypt::SecretBuffer, it can use a more official "C API" that doesn't rely on perl stringification behavior.
CONSTRUCTORS
new
$buf= Crypt::SecretBuffer->new($assign_value);
$buf= Crypt::SecretBuffer->new(%attrs);
If you pass one value to the constructor, it "assign"s that to the buffer. If you pass a list of key/value pairs, it assigns those attributes, such as ->new(capacity => 20)
. Technically it just calls each key as a method with the value as a single argument, so you could also do things like ->new(append_random => 16)
.
secret_buffer / secret
The functions secret_buffer
and secret
can be exported from this module as a shorthand for Crypt::SecretBuffer->new(...)
.
ATTRIBUTES
capacity
say $buf->capacity;
$buf->capacity($n_bytes)->...
$buf->capacity($n_bytes, AT_LEAST)->...
$buf->capacity($n_bytes, 'AT_LEAST')->...
This reads or writes the allocated length of the buffer, presumably because you know how much space you need for an upcoming read operation, but it can also free up space you know you no longer need. In the third example, a second parameter 'AT_LEAST' is passed to indicate that the buffer does not need reallocated if it is already large enough.
length
say $buf->length;
$buf->length(0); # wipes buffer
$buf->length(32); # fills with zeroes
This gets or sets the length of the string in the buffer. If you set it to a smaller value, the string is truncated. If you set it to a larger value, the "capacity" is raised as needed and the bytes are initialized with zeroes.
stringify_mask
$buf->stringify_mask; # "[REDACTED]"
$buf->stringify_mask("*****"); # now stringifies as "*****"
$buf->stringify_mask(undef); # exposes secret
Get or set the stringification mask. Setting it to undef
causes "stringify" to expose the secret. In order to restore the default "[REDACTED]"
you have to delete the attribute: delete $buf->{stringify_mask}
. This attribute is mainly intended to allow cusomizing the mask during the constructor. The preferred way to expose the secret is with local
on the hash key directly.
METHODS
clear
Erases the buffer. Equivalent to $buf->length(0)
. Returns $self
for chaining.
assign
$buf->assign($other_buf); # good
$buf->assign($string); # works, but $string isn't secret...
Assign a value to the buffer. Returns $self
, for chaining.
stringify
$buf->stringify; # returns "[REDACTED]"
$buf->{stringify_mask}= "***";
$buf->stringify; # returns "***"
$buf->{stringify_mask}= undef;
$buf->stringify; # returns secret value
do { local $buf->{stringify_mask}= undef; "$buf" } # expose secret once
SecretBuffer tries not to expose the secret, so the default behavior of this function is to return the string "[REDACTED]"
or whatever custom string you store in stringify_mask
. If you set stringify_mask
to undef
, it exposes the secret. You can use local
to limit the scope of this exposure.
index
$ofs= $buf->index($str);
$ofs= $buf->index($str, $from_offset);
Like Perl's index
function, returns -1 if not found, or else the offset of the start of the string you asked it to look for. You can specify an optional starting offset to search from. Negative starting offsets search from that many characters before the end of the buffer.
substr
$buf->substr(1); # New SecretBuffer minus the first character
$buf->substr(0,5); # First 5 characters of buffer
$buf->substr(0,5,$buf2); # replace first 5 characters with content of $buf2
This is exactly like Perl's substr
function, but it returns Crypt::SecretBuffer
objects, and they are not an lvalue that alters the original.
append_random
$byte_count= $buf->append_random($n_bytes);
$byte_count= $buf->append_random($n_bytes, NONBLOCK);
$byte_count= $buf->append_random($n_bytes, 'NONBLOCK');
Append N cryptographic-quality random bytes. On POSIX systems, this uses either the C library getrandom
call with GRND_RANDOM
, or if that isn't available, it reads from /dev/random
. The NONBLOCK
flag can be used to avoid blocking on insufficient entropy. On Windows, this uses CryptGenRandom
and the flag has no effect because it always returns the requested number of bytes and never blocks.
append_console_line
$bool= $buf->append_console_line(STDIN);
This turns off TTY echo (if the handle is a Unix TTY or Windows Console) and reads and appends characters until newline or EOF (and does not store the \r or \n characters). It returns true if the read "completed" with a line terminator, or false on EOF, or undef
on any OS error. Characters may be added to the buffer even when it returns false. There may also be no characters added when it returns true, if the user just hits <enter>.
When possible, this reads directly from the OS to avoid buffering the secret in libc or Perl, but reads from the buffer if you already have input data in one of those buffers, or if the file handle is a virtual Perl handle not backed by the OS.
append_sysread
$byte_count= $buf->append_sysread($fh, $count);
This performs a low-level read from the file handle and appends the bytes to the buffer. It must be a real file handle with an underlying file descriptor number (fileno
). Like sysread
, on error it returns undef
and on success it returns the count added. This ignores Perl I/O layers.
append_read
$byte_count= $buf->append_read($fh, $count);
This is a relaxed version of append_sysread
that when possible, reads directly from the OS to avoid buffering the secret in libc or Perl, but reads from the Perl buffer if you already have input data in one of those buffers, or if the file handle is a virtual Perl handle not backed by the OS.
syswrite
$byte_count= $buf->syswrite($fh); # one syswrite attempt of whole buffer
$byte_count= $buf->syswrite($fh, $count); # prefix of buffer
$byte_count= $buf->syswrite($fh, $count, $offset); # substr of buffer
This performs a low-level write from the buffer into a file handle. It must be a real file handle with an underlying file descriptor (fileno
). If the handle has pending bytes in its IO buffer, those are flushed first. Like syswrite
, this returns undef
on an OS error, and otherwise returns the number of bytes written. It only makes one write attempt, which may be shorter than the requested $count
. This ignores Perl I/O layers.
write_async
$async_result= $buf->write_async($fh); # whole buffer
$async_result= $buf->write_async($fh, $count); # prefix of buffer
$async_result= $buf->write_async($fh, $count, $offset); # substr of buffer
($wrote, $errno)= $async_result->wait;
($wrote, $errno)= $async_result->wait($seconds);
Write data into a file handle, using a background thread if needed. Most likely, you will be writing into a pipe, and your secret will be smaller than the OS pipe buffer, so this will complete immediately without spawning a thread. It also immediately returns if there was a fatal error attempting to write the handle. But if you have a large secret, or are writing into a type of handle that can't buffer it, this function will duplicate your file handle and copy the secret and pass them to a background thread to do the writing.
You can check the status or wait for its completion using the $async_result object.
as_pipe
$fh= $buf->as_pipe
This creates a pipe, then calls $self->write_async($pipe)
into the write-end of the pipe. You can then pass this pipe to other processes without needing to "pump" the pipe like you would with IPC::Run.
The $async_result
from "write_async" is ignored, allowing the background thread to complete (or error on a closed pipe) on its own time.
EXPORTS
- AT_LEAST
-
Parameter for setting the "capacity".
- NONBLOCK
-
Parameter for "append_random".
- secret_buffer
-
Shorthand function for calling "new".
- secret
-
Shorthand function for calling "new".
C API
This module is intended for C code as much as it is for Perl code. To write an XS module that uses SecretBuffer, your XS module should use ExtUtils::Depends to add the headers and linkage needed for the C API:
my $dep= ExtUtils::Depends->new('Your::Module', 'Crypt::SecretBuffer');
...
WriteMakefile(
'NAME' => 'Mymodule',
$dep->get_makefile_vars()
);
You can also just use it with Inline::C if you want to skip the hassle of an XS module:
package TestSecretBufferWithInline;
use strict;
use warnings;
use Inline with => 'Crypt::SecretBuffer';
use Inline C => <<END_C;
#include <SecretBuffer.h>
int test(secret_buffer *buf) {
return buf->len;
}
END_C
print test(Crypt::SecretBuffer->new(length => 10))."\n";
1;
The complete API documentation is found in SecretBuffer.h
Reporting Security Vulnerabilities
Security issues should not be reported on the bugtracker website. Please see SECURITY.md for instructions how to report security vulnerabilities.
VERSION
version 0.005
AUTHOR
Michael Conrad <[email protected]>
COPYRIGHT AND LICENSE
This software is copyright (c) 2025 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.