Group
Extension

String-Secret/lib/String/Secret.pm

package String::Secret;
use 5.008001;
use strict;
use warnings;

use Scalar::Util qw/refaddr/;

our $VERSION = "0.01";

use overload
    '""' => 'to_string',
    fallback => 1;

our $DISABLE_MASK = 0;
our $MASKED_STRING = '*' x 8;

my %SECRETS;

sub new {
    my ($class, $secret) = @_;
    my $masked = $MASKED_STRING;
    my $self = bless \$masked, $class;
    $SECRETS{refaddr($self)} = $secret;
    return $self;
}

sub from_serializable { $_[0]->new($_[1]->unwrap) }

sub unwrap { $SECRETS{refaddr($_[0])} }

sub to_serializable {
    require String::Secret::Serializable;
    String::Secret::Serializable->new(shift->unwrap);
}

sub to_string {
    return shift->unwrap if $DISABLE_MASK;
    return $MASKED_STRING;
}

# for Storable
sub STORABLE_freeze {
    my ($self, $cloning) = @_;
    return $self->unwrap if $cloning;
    return $self->to_string;
}
sub STORABLE_thaw {
    my ($self, $cloning, $masked) = @_;
    if ($cloning) {
        $SECRETS{refaddr($self)} = $masked; # $masked is unwrapped value
        return $self;
    }

    die "cannot deserialize it, should convert it as serializable by \$secret->to_serializable";
}

# for JSON modules
sub TO_JSON { shift->to_string }

# for CBOR
sub TO_CBOR { shift->to_string }

# for JSON, CBOR, Sereal, ...
sub FREEZE { shift->to_string }
sub THAW {
    die "cannot deserialize it, should convert it as serializable by \$secret->to_serializable";
}

# for Data::Clone
sub clone { shift } # immutable

sub DESTROY { delete $SECRETS{refaddr($_[0])} }

1;
__END__

=encoding utf-8

=for stopwords serializable

=head1 NAME

String::Secret - secret string wrapper to mask secret from logger

=head1 SYNOPSIS

    use String::Secret;
    use String::Compare::ConstantTime;
    use JSON::PP ();

    my $secret = String::Secret->new('mysecret');

    # safe secret for logging
    MyLogger->warn("invalid secret: $secret"); # oops! but the secret is hidden: "invalid secret: ********"

    # and safe secret for serialization
    # MyLogger->warn("invalid secret: ".JSON::PP->new->allow_tags->encode({ secret => $secret })); # oops! but the secret is hidden: invalid secret: {"secret":"********"}

    unless (String::Compare::ConstantTime::equals($secret->unwrap, SECRET)) {
        die "secret mis-match";
    }

    # and can it convert to serializable
    MyDB->credentials->new(
        id     => 'some id',
        secret => $secret->to_serializable, # or $secret->unwrap
    )->save();


=head1 DESCRIPTION

String::Secret is a secret string wrapper to mask secret from logger.

=head1 LICENSE

Copyright (C) karupanerura.

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

=head1 AUTHOR

karupanerura E<lt>karupa@cpan.orgE<gt>

=cut



Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.