File Coverage

blib/lib/Crypt/SecretBuffer/PEM.pm
Criterion Covered Total %
statement 110 115 95.6
branch 43 62 69.3
condition 12 24 50.0
subroutine 15 15 100.0
pod 9 9 100.0
total 189 225 84.0


line stmt bran cond sub pod time code
1             package Crypt::SecretBuffer::PEM;
2             # VERSION
3             # ABSTRACT: Parse PEM format from a SecretBuffer
4             $Crypt::SecretBuffer::PEM::VERSION = '0.021';
5 1     1   464 use strict;
  1         1  
  1         24  
6 1     1   3 use warnings;
  1         2  
  1         30  
7 1     1   3 use Carp;
  1         1  
  1         42  
8 1     1   3 use Scalar::Util qw( blessed );
  1         1  
  1         31  
9 1     1   3 use Crypt::SecretBuffer qw/ secret span MATCH_NEGATE MATCH_REVERSE MATCH_ANCHORED MATCH_MULTI ISO8859_1 BASE64 /;
  1         1  
  1         4  
10              
11              
12             sub parse {
13 10     10 1 205487 my ($class, $span, %options)= @_;
14 10         27 my $secret_headers= !!$options{secret_headers};
15 10 50       33 my $notrim= !exists $options{trim_headers}? 0 : !$options{trim_headers};
16 10         141 while (my $begin= $span->scan("-----BEGIN ")) {
17 9         40 $span->pos($begin->lim);
18 9         96 my $label= $span->parse(qr/[A-Z0-9 ]+/);
19 9 50 33     42 next unless $label && $span->parse("-----\n");
20 9         1945 $begin->lim($span->pos);
21 9         18 my $label_str= '';
22 9         61 $label->copy_to($label_str);
23             # back up the span by 1 char so that it starts with \n, just in case there is an END
24             # line immediately following the BEGIN line.
25 9         36 $span->pos($span->pos-1);
26 9         74 my $end= $span->scan("\n-----END $label_str-----");
27 9 50       30 unless ($end) {
28 0         0 carp "PEM begin marker for $label_str lacks an END marker";
29 0         0 next;
30             }
31 9         37 $end->ltrim("\n");
32 9         28 $span->pos($end->lim);
33 9         31 $span->ltrim("\r");
34 9         25 $span->ltrim("\n"); # consume line ending
35              
36             # Let block be its own SecretBuffer
37 9         98 my $block= $span->clone(pos => $begin->pos, lim => $span->pos)->copy;
38 9         79 my $inner= $block->span(pos => $begin->len, lim => $end->pos - $begin->pos);
39 9 50       81 if (!$block->span($end->pos - $begin->pos - 1, 1)->ends_with("\n")) {
40 0         0 carp "PEM end marker found not at the start of a line";
41 0         0 next;
42             }
43             # Treat each line containing a ":" as a "name: value" header
44 9         25 my @headers;
45 9         73 while (my $sep_or_eol= $inner->scan(qr/[:\n]/)) {
46 13 100       50 if ($sep_or_eol->starts_with(':')) {
47 5         10 my ($name, $value);
48 5         59 my $name_span= $inner->clone(lim => $sep_or_eol->pos);
49 5 50       89 $name_span->trim unless $notrim;
50 5         24 $name_span->copy_to($name);
51 5         17 $inner->pos($sep_or_eol->lim);
52 5 50       28 my $eol= $inner->scan("\n") or die "BUG"; # inner ends with "\n", checked above
53 5         26 my $val_span= $inner->clone(lim => $eol->pos);
54             # notrim means don't remove arbitrary leading/trailing whitespace.
55             # the space char after ':' is part of the specification, so should be removed.
56 5 50       30 $notrim? $val_span->ltrim(' ') : $val_span->trim;
57 5         27 $inner->pos($eol->lim);
58 5 100       12 if ($secret_headers) {
59 2         23 push @headers, $name, $val_span;
60             } else {
61 3         14 $val_span->copy_to($value);
62 3         35 push @headers, $name, $value;
63             }
64             }
65             else {
66             # If any headers were found, there needs to be a blank line
67 8 100       19 if (@headers) {
68 3 50       14 if ($sep_or_eol->pos == $inner->pos) { # "\n" at start of 'inner'
69 3         41 $inner->pos($sep_or_eol->lim);
70             } else {
71 0         0 carp "PEM headers for $label_str did not end with a blank line"
72             }
73             }
74 8         21 last;
75             }
76             }
77 9         83 $inner->encoding(BASE64);
78 9         35 return $class->new(
79             buffer => $block,
80             label => $label_str,
81             header_kv => \@headers,
82             content => $inner,
83             );
84             }
85 1         4 return undef;
86             }
87              
88             sub parse_all {
89 1     1 1 7107 my ($class, $span, %options)= @_;
90 1         3 my @pem;
91 1         7 while (my $pem= $class->parse($span, %options)) {
92 3         15 push @pem, $pem;
93             }
94 1         7 return @pem;
95             }
96              
97             sub new {
98 10     10 1 6973 my $class= shift;
99 10         51 my $self= bless {}, $class;
100 10         30 while (@_) {
101 38         93 my ($attr, $val)= splice(@_, 0, 2);
102 38         100 $self->$attr($val);
103             }
104 10         113 $self;
105             }
106              
107              
108 9 50   9 1 40 sub buffer { $_[0]{buffer}= $_[1] if @_ > 1; $_[0]{buffer} }
  9         24  
109 30 100   30 1 13988 sub label { $_[0]{label}= $_[1] if @_ > 1; $_[0]{label} }
  30         140  
110 23 100   23 1 2582 sub content { $_[0]{content}= $_[1] if @_ > 1; $_[0]{content} }
  23         84  
111             sub header_kv {
112 25 100   25 1 1050 if (@_ > 1) {
113 10         38 _validate_header_kv($_[1]);
114 10         52 $_[0]{header_kv}= $_[1];
115             $_[0]{headers}->raw_kv_array($_[1])
116 10 50       36 if defined $_[0]{headers};
117             }
118             $_[0]{header_kv}
119 25         90 }
120             sub headers {
121 6     6 1 3129 my $self= shift;
122 6         49 require Crypt::SecretBuffer::PEM::Headers;
123             $self->{headers} ||=
124 6   66     38 Crypt::SecretBuffer::PEM::Headers
125             ->new(raw_kv_array => $self->header_kv)
126             ->_create_tied_hashref;
127             }
128              
129             sub _validate_header_kv {
130 13     13   22 my $kv= shift;
131 13 50 33     92 croak "Expected even-length arrayref"
132             unless ref $kv eq 'ARRAY' && ($#$kv & 1);
133 13         57 for (0..($#$kv-1)/2) {
134 13         47 my ($k, $v)= ($kv->[$_*2], $kv->[$_*2+1]);
135 13 50       33 croak "PEM header Key is undefined"
136             unless defined $k;
137 13 50       42 croak "PEM Header name '$k' contains wide characters"
138             unless utf8::downgrade($k, 1);
139             # Sanity checks, key cannot contain control chars or ':' or leading or trailing whitespace
140 13 50       46 croak "PEM Header name '$k' contains ':' or control characters"
141             if $k =~ /[\0-\x1F\x7F:]/;
142 13 100 66     328 carp "PEM header name '$k' contains leading/trailing whitespace"
143             if $k =~ /^\s/ or $k =~ /\s\z/;
144 13 50       32 croak "PEM header value for '$k' is undefined"
145             unless defined $v;
146 13   66     43 my $is_secret= blessed($v) && ($v->isa('Crypt::SecretBuffer::Span') || $v->isa('Crypt::SecretBuffer'));
147 13 50 66     51 croak "PEM header value for $k' contains wide characters"
148             unless $is_secret || utf8::downgrade($v, 1);
149 13 100       58 croak "PEM header value for '$k' contains control characters"
    50          
150             if $is_secret? ($v->scan(qr/[\0-\x1F\x7F]/))
151             : ($v =~ /[\0-\x1F\x7F]/);
152 13 100 33     254 carp "PEM header value for '$k' contains leading/trailing whitespace"
    50 33        
153             if $is_secret? ($v->scan(qr/[\s]/, MATCH_ANCHORED) or $v->scan(qr/[\s]/, MATCH_ANCHORED|MATCH_REVERSE))
154             : ($v =~ /^\s/ or $v =~ /\s\z/);
155             }
156 13         45 1;
157             }
158              
159              
160             sub serialize {
161 6     6 1 5063 my $self= shift;
162 6         42 my $out= secret('-----BEGIN '.$self->label."-----\n");
163 6 50       15 my @header_kv= @{ $self->header_kv || [] };
  6         16  
164 6 100       14 if (@header_kv) {
165             # re-validate since individual values are mutable and may have changed since the
166             # attribute was assigned.
167 3         10 _validate_header_kv(\@header_kv);
168 3         9 for (0..$#header_kv) {
169 10 100       54 $out->append($header_kv[$_])->append($_ & 1? "\n" : ": ");
170             }
171 3         8 $out->append("\n"); # empty line terminates headers
172             }
173 6         15 my $content_span= span($self->content);
174 6         52 $content_span->append_to($out, encoding => BASE64);
175 6 100       24 $out->append(($content_span->length? "\n" : '')
176             .'-----END '.$self->label."-----\n");
177 6         62 return $out;
178             }
179              
180             # avoid depending on namespace::clean
181             delete @{Crypt::SecretBuffer::PEM::}{qw(
182             carp confess croak blessed secret span MATCH_NEGATE MATCH_REVERSE MATCH_ANCHORED MATCH_MULTI
183             ISO8859_1 BASE64
184             )};
185              
186             1;
187              
188             __END__