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 vars qw( $VERSION ); $VERSION = '0.08'; sub new { my $class = shift; my $pem = bless { }, $class; $pem->init(@_); } sub init { my $pem = shift; my %param = @_; unless (exists $param{ASN} && exists $param{Name}) { return (ref $pem)->error("init: Name and ASN are required"); } else { $pem->{ASN} = $param{ASN}; $pem->{Name} = $param{Name}; } $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; } sub asn { $_[0]->{_asn} } sub ASN { $_[0]->{ASN} } sub name { $_[0]->{Name} } sub read { my $pem = shift; my %param = @_; my $blob; local *FH; my $fname = delete $param{Filename}; open FH, $fname or return $pem->error("Can't open $fname: $!"); $blob = do { local $/; }; close FH; $param{Content} = $blob; $pem->decode(%param); } sub write { my $pem = shift; my %param = @_; my $fname = delete $param{Filename} or return $pem->error("write: Filename is required"); my $blob = $pem->encode(%param); local *FH; open FH, ">$fname" or return $pem->error("Can't open $fname: $!"); print FH $blob; close FH; $blob; } sub decode { my $pem = shift; my %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; } my $asn = $pem->asn; if (my $macro = ($param{Macro} || $pem->{Macro})) { $asn = $asn->find($macro) or return $pem->error("Can't find Macro $macro"); } my $obj = $asn->decode($buf) or return $pem->error("ASN decode failed: $asn->{error}"); $obj; } sub encode { my $pem = shift; my %param = @_; my $asn = $pem->asn; if (my $macro = ($param{Macro} || $pem->{Macro})) { $asn = $asn->find($macro) or return $pem->error("Can't find Macro $macro"); } my $buf = $asn->encode( $param{Content} ) or return $pem->error("ASN encode failed: $asn->{error}"); my(@headers); if ($param{Password}) { my($info); ($buf, $info) = $pem->encrypt( Plaintext => $buf, Password => $param{Password} ) 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}, '' ); $content =~ s!(.{1,64})!$1\n!g; my $headers = join '', map { "$_->[0]: $_->[1]\n" } @{ $param{Headers} }; $headers .= "\n" if $headers; "$head\n$headers$content$tail\n"; } use vars qw( %CTYPES ); %CTYPES = ('DES-EDE3-CBC' => 'Crypt::DES_EDE3'); sub decrypt { my $pem = shift; my %param = @_; my $passphrase = $param{Password} || ""; my($ctype, $iv) = split /,/, $param{Info}; my $cmod = $CTYPES{$ctype} or return $pem->error("Unrecognized cipher: '$ctype'"); $iv = pack "H*", $iv; my $cbc = Convert::PEM::CBC->new( Passphrase => $passphrase, Cipher => $cmod, IV => $iv ); my $buf = $cbc->decrypt($param{Ciphertext}) or return $pem->error("Decryption failed: " . $cbc->errstr); $buf; } sub encrypt { my $pem = shift; my %param = @_; $param{Password} or return $param{Plaintext}; my $ctype = $param{Cipher} || 'DES-EDE3-CBC'; my $cmod = $CTYPES{$ctype} or return $pem->error("Unrecognized cipher: '$ctype'"); my $cbc = Convert::PEM::CBC->new( Passphrase => $param{Password}, Cipher => $cmod ); my $iv = uc join '', unpack "H*", $cbc->iv; my $buf = $cbc->encrypt($param{Plaintext}) or return $pem->error("Encryption failed: " . $cbc->errstr); ($buf, "$ctype,$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", 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 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 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 and C) indicate information about the type of encryption used, and the string starting with C is the Base64-encoded, encrypted, ASN.1-encoded contents of this "object." The initialization vector (C) is chosen randomly. =head1 USAGE =head2 $pem = Convert::PEM->new( %arg ) Constructs a new I object designed to read/write an object of a specific type (given in I<%arg>, see below). Returns the new object on success, C on failure (see I 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. Similarly, when encoding an object, the value of I will be used as the name of the object in the PEM-encoded content. For example, given the string C, the output from I will start with a header like: -----BEGIN FOO BAR----- I is a required argument. =item * ASN An ASN.1 description of the content to be either encoded or decoded. I is a required argument. =item * Macro If your ASN.1 description (in the I parameter) includes more than one ASN.1 macro definition, you will want to use the I 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 object, you will need to tell I to use the C macro definition by using the I parameter and setting the value to C. I is an optional argument. =back =head2 $obj = $pem->decode(%args) Decodes, and, optionally, decrypts a PEM file, returning the object as decoded by I. The difference between this method and I is that I 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, and you should check the error message using the I 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. Returns I on failure (encryption failure, file-writing failure, etc.); in this case you should check the error message using the I method (below). On success returns the constructed PEM string. I<%args> can contain: =over 4 =item * Content A hash reference that will be passed to I, and which should correspond to the ASN.1 description you gave to the I method. The hash reference should have the exact same format as that returned from the I method. This argument is mandatory. =item * Password A password used to encrypt the contents of the PEM file. This is an optional argument; if not provided the contents will be unencrypted. =back =head2 $obj = $pem->read(%args) Reads, decodes, and, optionally, decrypts a PEM file, returning the object as decoded by I. This is implemented as a wrapper around I, with the bonus of reading the PEM file from disk for you. If an error occurs while reading the file or decrypting/decoding the contents, the function returns I, and you should check the error message using the I method (below). In addition to the arguments that can be passed to the I method (minus the I method), I<%args> can contain: =over 4 =item * Filename The location of the PEM file that you wish to read. =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, with the bonus of writing the file to disk for you. Returns I on failure (encryption failure, file-writing failure, etc.); in this case you should check the error message using the I method (below). On success returns the constructed PEM string. In addition to the arguments for I, I<%args> can contain: =over 4 =item * Filename The location on disk where you'd like the PEM file written. =back =head2 $pem->errstr Returns the value of the last error that occurred. This should only be considered meaningful when you've received I from one of the functions above; in all other cases its relevance is undefined. =head2 $pem->asn Returns the I 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 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' } ); =head1 ERROR HANDLING If an error occurs in any of the above methods, the method will return C. You should then call the method I to determine the source of the error: $pem->errstr In the case that you do not yet have a I object (that is, if an error occurs while creating a I object), the error can be obtained as a class method: Convert::PEM->errstr For example, if you try to decode an encrypted object, and you do not give a passphrase to decrypt the object: my $obj = $pem->read( Filename => "encrypted.pem" ) or die "Decryption failed: ", $pem->errstr; =head1 LICENSE Convert::PEM is free software; you may redistribute it and/or modify it under the same terms as Perl itself. =head1 AUTHOR & COPYRIGHTS Except where otherwise noted, Convert::PEM is Copyright Benjamin Trott, cpan@stupidfool.org. All rights reserved. =cut