Convert-PEM
view release on metacpan or search on metacpan
lib/Convert/PEM.pm view on Meta::CPAN
package Convert::PEM;
use strict;
use 5.008_001;
use base qw( Class::ErrorHandler );
use MIME::Base64;
use Digest::MD5 qw( md5 );
use Convert::ASN1;
use Carp qw( croak );
use Convert::PEM::CBC;
use Crypt::PRNG qw( random_bytes );
use vars qw( $VERSION $DefaultCipher );
our $VERSION = '0.13'; # VERSION
our $DefaultCipher = 'DES-EDE3-CBC';
sub new {
my $class = shift;
my $pem = bless { }, $class;
$pem->init(@_);
}
sub init {
my $pem = shift;
my %param = @_;
unless (exists $param{Name}) {
return (ref $pem)->error("init: Name is required");
}
else {
$pem->{Name} = $param{Name};
$pem->{ASN} = $param{ASN} if exists $param{ASN};
$pem->{Cipher} = $param{Cipher} if exists $param{Cipher};
}
if (exists $pem->{ASN}) {
$pem->{Macro} = $param{Macro};
my $asn = $pem->{_asn} = Convert::ASN1->new;
$asn->prepare( $pem->{ASN} ) or
return (ref $pem)->error("ASN prepare failed: $asn->{error}");
}
$pem->_getform(%param) or return;
$pem;
}
sub _getform {
my $pem = shift;
my %param = @_;
my $in = defined $param{InForm} ? uc($param{InForm}) : 'PEM';
$in =~ m/^(PEM|DER)$/ or return $pem->error("Invalid InForm '$in': must be PEM or DER");
$pem->{InForm} = $in;
my $out = defined $param{OutForm} ? uc($param{OutForm}) : 'PEM';
$out =~ m/^(PEM|DER)$/ or return $pem->error("Invalid OutForm '$out': must be PEM or DER");
$pem->{OutForm} = $out;
$pem;
}
sub asn {
my $pem = shift;
my $asn = $pem->{_asn} || return;
my %prm = @_;
my $m = $prm{Macro} || $pem->{Macro};
$m ? $asn->find($m) : $asn;
}
sub ASN { $_[0]->{ASN} }
sub name { $_[0]->{Name} }
sub cipher { $_[0]->{Cipher} }
sub inform { $_[0]->{InForm} }
sub outform { $_[0]->{OutForm} }
sub macro { $_[0]->{Macro} }
sub read {
my $pem = shift;
my %param = @_;
my $blob;
my $fname = delete $param{Filename};
open my $FH, $fname or
return $pem->error("Can't open $fname: $!");
binmode $FH;
read($FH, $blob, -s $fname);
close $FH;
$pem->{InForm} eq 'DER'
? $pem->from_der( DER => $blob )
: $pem->decode(%param, Content => $blob);
}
sub write {
my $pem = shift;
my %param = @_;
my $fname = delete $param{Filename} or
return $pem->error("write: Filename is required");
my $blob = $pem->{OutForm} eq 'DER'
? $pem->to_der(%param)
: $pem->encode(%param);
open my $FH, ">$fname" or
return $pem->error("Can't open $fname: $!");
binmode $FH;
print $FH $blob;
close $FH;
$blob;
}
sub from_der {
my $pem = shift;
my %param = @_;
# should always be unencrypted at this point
my $obj;
if (exists $pem->{ASN}) {
my $asn = $pem->asn;
if (my $macro = ($param{Macro} || $pem->{Macro})) {
$asn = $asn->find($macro) or
return $pem->error("Can't find Macro $macro");
}
$obj = $asn->decode( $param{DER} ) or
return $pem->error("ASN encode failed: $asn->{error}");
}
else {
$obj = $param{DER};
}
$obj;
}
sub decode {
my $pem = shift;
my %param = @_;
if (exists $param{DER}) { return $pem->from_der(%param) }
my $blob = $param{Content} or
return $pem->error("'Content' is required");
chomp $blob;
my $dec = $pem->explode($blob) or return;
my $name = $param{Name} || $pem->name;
return $pem->error("Object $dec->{Object} does not match " . $name)
unless $dec->{Object} eq $name;
my $head = $dec->{Headers};
my $buf = $dec->{Content};
my %headers = map { $_->[0] => $_->[1] } @$head;
if (%headers && $headers{'Proc-Type'} eq '4,ENCRYPTED') {
$buf = $pem->decrypt( Ciphertext => $buf,
Info => $headers{'DEK-Info'},
Password => $param{Password} )
or return;
}
$param{DER} = $buf;
my $obj = $pem->from_der( %param )
or return;
$obj;
}
sub to_der {
my $pem = shift;
my %param = @_;
my $buf;
if (exists $pem->{ASN}) {
my $asn = $pem->asn;
if (my $macro = ($param{Macro} || $pem->{Macro})) {
$asn = $asn->find($macro) or
return $pem->error("Can't find Macro $macro");
}
$buf = $asn->encode( $param{Content} ) or
return $pem->error("ASN encode failed: $asn->{error}");
}
else {
$buf = $param{Content}
}
$buf;
}
sub encode {
my $pem = shift;
my %param = @_;
my $buf = $param{DER} || $pem->to_der(%param);
my (@headers);
if ($param{Password}) {
my ($info);
($buf, $info) = $pem->encrypt( Plaintext => $buf,
%param )
or return;
push @headers, [ 'Proc-Type' => '4,ENCRYPTED' ];
push @headers, [ 'DEK-Info' => $info ];
}
$pem->implode( Object => $param{Name} || $pem->name,
Headers => \@headers,
Content => $buf );
}
sub explode {
my $pem = shift;
my ($message) = @_;
# Canonicalize line endings into "\n".
$message =~ s/\r\n|\n|\r/\n/g;
my ($head, $object, $headers, $content, $tail) = $message =~
m:(-----BEGIN ([^\n\-]+)-----)\n(.*?\n\n)?(.+)(-----END .*?-----)$:s;
my $buf = decode_base64($content);
my @headers;
if ($headers) {
for my $h ( split /\n/, $headers ) {
my ($k, $v) = split /:\s*/, $h, 2;
push @headers, [ $k => $v ] if $k;
}
}
{ Content => $buf,
Object => $object,
Headers => \@headers }
}
sub implode {
my $pem = shift;
my %param = @_;
my $head = "-----BEGIN $param{Object}-----";
my $tail = "-----END $param{Object}-----";
my $content = encode_base64( $param{Content}, '' );
lib/Convert/PEM.pm view on Meta::CPAN
$buf;
}
sub encrypt {
my $pem = shift;
my %param = @_;
$param{Password} or return $param{Plaintext};
$param{Cipher} = $DefaultCipher if !$param{Cipher};
my $ctype = $pem->has_cipher( $param{Cipher} );
my $cmod = $CTYPES{$ctype} or
return $pem->error("Unrecognized cipher: '$ctype'");
eval "use $cmod->{c}; 1;" || croak "Error loading cypher module '$cmod->{c}'";
## allow custom IV for encryption
my $iv = $pem->_getiv(%param, bs => $cmod->{bs}) or return;
my $key = Convert::PEM::CBC::bytes_to_key( $param{Password}, $iv, \&md5, $cmod->{ks} );
my $cm = $cmod->{c}; $cm =~ s/^Crypt::(?=IDEA$)//; # fix IDEA
my $cbc = Convert::PEM::CBC->new(
IV => $iv,
Cipher => $cm->new($key) );
$iv = uc join '', unpack "H*", $cbc->iv;
my $buf = $cbc->encrypt($param{Plaintext}) or
return $pem->error("Encryption failed: " . $cbc->errstr);
($buf, "$ctype,$iv");
}
sub _getiv {
my $pem = shift;
my %p = @_;
my $iv;
if (exists $p{IV}) {
if ($p{IV} =~ m/^[a-fA-F\d]+$/) {
$iv = pack("H*",$p{IV});
return length($iv) == $p{bs}
? $iv
: $pem->error("Provided IV length is invalid");
}
else {
return $pem->error("Provided IV must be in hex format");
}
}
$iv = random_bytes($p{bs});
croak "Internal error: unexpected IV length" if length($iv) != $p{bs};
$iv;
}
1;
__END__
=head1 NAME
Convert::PEM - Read/write encrypted ASN.1 PEM files
=head1 SYNOPSIS
use Convert::PEM;
my $pem = Convert::PEM->new(
Name => "DSA PRIVATE KEY",
Macro => "DSAPrivateKey",
ASN => qq(
DSAPrivateKey SEQUENCE {
version INTEGER,
p INTEGER,
q INTEGER,
g INTEGER,
pub_key INTEGER,
priv_key INTEGER
}
));
my $keyfile = 'private-key.pem';
my $pwd = 'foobar';
my $pkey = $pem->read(
Filename => $keyfile,
Password => $pwd
);
$pem->write(
Content => $pkey,
Password => $pwd,
Filename => $keyfile
);
=head1 DESCRIPTION
I<Convert::PEM> reads and writes PEM files containing ASN.1-encoded
objects. The files can optionally be encrypted using a symmetric
cipher algorithm, such as 3DES. An unencrypted PEM file might look
something like this:
-----BEGIN DH PARAMETERS-----
MB4CGQDUoLoCULb9LsYm5+/WN992xxbiLQlEuIsCAQM=
-----END DH PARAMETERS-----
The string beginning C<MB4C...> is the Base64-encoded, ASN.1-encoded
"object."
An encrypted file would have headers describing the type of
encryption used, and the initialization vector:
-----BEGIN DH PARAMETERS-----
Proc-Type: 4,ENCRYPTED
DEK-Info: DES-EDE3-CBC,C814158661DC1449
AFAZFbnQNrGjZJ/ZemdVSoZa3HWujxZuvBHzHNoesxeyqqidFvnydA==
-----END DH PARAMETERS-----
The two headers (C<Proc-Type> and C<DEK-Info>) indicate information
about the type of encryption used, and the string starting with
C<AFAZ...> is the Base64-encoded, encrypted, ASN.1-encoded
contents of this "object."
The initialization vector (C<C814158661DC1449>) is chosen randomly.
=head1 USAGE
=head2 $pem = Convert::PEM->new( %arg )
Constructs a new I<Convert::PEM> object designed to read/write an
object of a specific type (given in I<%arg>, see below). Returns the
new object on success, C<undef> on failure (see I<ERROR HANDLING> for
details).
I<%arg> can contain:
=over 4
=item * Name
The name of the object; when decoding a PEM-encoded stream, the name
in the encoding will be checked against the value of I<Name>.
Similarly, when encoding an object, the value of I<Name> will be used
as the name of the object in the PEM-encoded content. For example, given
the string C<FOO BAR>, the output from I<encode> will start with a
header like:
-----BEGIN FOO BAR-----
I<Name> is a required argument.
=item * ASN
An ASN.1 description of the content to be either encoded or decoded.
I<ASN> is an optional argument.
=item * Macro
If your ASN.1 description (in the I<ASN> parameter) includes more than
one ASN.1 macro definition, you will want to use the I<Macro> parameter
to specify which definition to use when encoding/decoding objects.
For example, if your ASN.1 description looks like this:
Foo ::= SEQUENCE {
x INTEGER,
bar Bar
}
Bar ::= INTEGER
If you want to encode/decode a C<Foo> object, you will need to tell
I<Convert::PEM> to use the C<Foo> macro definition by using the I<Macro>
parameter and setting the value to C<Foo>.
I<Macro> is an optional argument when an ASN.1 description is provided.
=item * InForm
Specify what type of file to expect when using the I<read> method. Value
may be either B<PEM> or B<DER>. Default is "PEM".
If "DER" is specified, encryption options are ignored when using the
I<read> method and file is read as an unencrypted blob. This option does
not affect the I<decode> behavior.
I<InForm> is an optional argument.
=item * OutForm
Specify what type of file the I<write> method should output. Value may be
either B<PEM> or B<DER>. Default is "PEM".
If "DER" is specified, encryption options are ignored when using the
I<write> method and the file is written as an uncrypted blob. This option
does not affect the I<encode> behavior.
I<OutForm> is an optional argument.
=back
=head2 $obj = $pem->decode(%args)
Decodes, and, optionally, decrypts a PEM file, returning the
object as decoded by I<Convert::ASN1>. The difference between this
method and I<read> is that I<read> reads the contents of a PEM file
on disk; this method expects you to pass the PEM contents as an
argument.
If an error occurs while reading the file or decrypting/decoding
the contents, the function returns I<undef>, and you should check
the error message using the I<errstr> method (below).
I<%args> can contain:
=over 4
=item * Content
The PEM contents.
=item * Password
The password with which the file contents were encrypted.
If the file is encrypted, this is a mandatory argument (well, it's
not strictly mandatory, but decryption isn't going to work without
it). Otherwise it's not necessary.
=back
=head2 $blob = $pem->encode(%args)
Constructs the contents for the PEM file from an object: ASN.1-encodes
the object, optionally encrypts those contents.
lib/Convert/PEM.pm view on Meta::CPAN
If an error occurs while reading the file or decrypting/decoding
the contents, the function returns I<undef>, and you should check
the error message using the I<errstr> method (below).
In addition to the arguments that can be passed to the I<decode>
method (minus the I<Content> argument), I<%args> can contain:
=over 4
=item * Filename
The location of the PEM file that you wish to read.
=item * InForm
Specify what file type to read. Description can be found under I<new>
method. If specified, will override InForm provided in I<new> method.
=back
=head2 $pem->write(%args)
Constructs the contents for the PEM file from an object: ASN.1-encodes
the object, optionally encrypts those contents; then writes the file
to disk. This is implemented as a wrapper around I<encode>, with the
bonus of writing the file to disk for you.
Returns I<undef> on failure (encryption failure, file-writing failure,
etc.); in this case you should check the error message using the
I<errstr> method (below). On success returns the constructed PEM string.
In addition to the arguments for I<encode>, I<%args> can contain:
=over 4
=item * Filename
The location on disk where you'd like the PEM file written.
=item * OutForm
Specify format to write out. Description can be found under I<new> method.
If specified, will override OutForm provided in I<new> method.
=back
=head2 $pem->from_der(%args)
Method used internally, but may be accessed directly decode an ASN.1 string
into a perl structure object. If the Convert::PEM object has no ASN.1
definition, this method has no effect.
=over 4
=item * DER
Binary string to convert to an object.
This option is required.
=item * Macro
If the object has an ASN definition, a Macro may be specified. If specified,
it will override the object's Macro if one exists.
Macro is an optional argument.
=back
=head2 $pem->to_der(%args)
Method used internally, but may be accessed directly to encode Content into
binary data. If the Convert::PEM object has no ASN.1 definition, this
method has no effect.
=over 4
=item * Content
An object to be ASN.1 encoded to a binary string.
=item * Macro
If the object has an ASN definition, a Macro may be specified. If specified,
it will override the object's Macro if one exists.
Macro is an optional argument.
=back
=head2 $pem->errstr
Returns the value of the last error that occurred. This should only
be considered meaningful when you've received I<undef> from one of
the functions above; in all other cases its relevance is undefined.
=head2 $pem->asn
Returns the I<Convert::ASN1> object used internally to decode and
encode ASN.1 representations. This is useful when you wish to
interact directly with that object; for example, if you need to
call I<configure> on that object to set the type of big-integer
class to be used when decoding/encoding big integers:
$pem->asn->configure( decode => { bigint => 'Math::Pari' },
encode => { bigint => 'Math::Pari' } );
=head2 $pem->inform
Retruns the I<InForm> configured for the object.
=head2 $pem->outform
Retruns the I<OutForm> configured for the object.
=head2 $pem->cipher
Returns the I<Cipher> configured for the object.
=head2 $pem->name
Returns the PEM I<Name> of the object.
=head1 CONFIGURATION
To support any encryption/decryption, the appropriate cipher module needs
to be installed.
Some settings may be viewed or configured through variables or methods.
Configuration settings are global to the package. If a setting is changed,
it affects all Convert::PEM objects.
=head2 $Convert::PEM::DefaultCipher I<or> $OBJ->DefaultCipher(I<[NEW_CIPHER]>)
Used to configure a default cipher when writing to the disk. When using
the method form C< $OBJ->DefaultCipher([NEW_CIPHER]) >, if NEW_CIPHER
is not specified, will return the current setting. If the specified
cipher is not recognized/valid, an error will be raised.
To list supported ciphers, use C<Convert::PEM::list_ciphers>. Here is a
list of supported Ciphers:
=over 4
=item * DES-CBC
( run in 0.733 second using v1.01-cache-2.11-cpan-d8267643d1d )