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.020';
5 1     1   760 use strict;
  1         3  
  1         42  
6 1     1   6 use warnings;
  1         2  
  1         56  
7 1     1   7 use Carp;
  1         2  
  1         81  
8 1     1   8 use Scalar::Util qw( blessed );
  1         2  
  1         101  
9 1     1   8 use Crypt::SecretBuffer qw/ secret span MATCH_NEGATE MATCH_REVERSE MATCH_ANCHORED MATCH_MULTI ISO8859_1 BASE64 /;
  1         1  
  1         8  
10              
11              
12             sub parse {
13 10     10 1 261124 my ($class, $span, %options)= @_;
14 10         28 my $secret_headers= !!$options{secret_headers};
15 10 50       34 my $notrim= !exists $options{trim_headers}? 0 : !$options{trim_headers};
16 10         200 while (my $begin= $span->scan("-----BEGIN ")) {
17 9         46 $span->pos($begin->lim);
18 9         128 my $label= $span->parse(qr/[A-Z0-9 ]+/);
19 9 50 33     42 next unless $label && $span->parse("-----\n");
20 9         60 $begin->lim($span->pos);
21 9         18 my $label_str= '';
22 9         66 $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         83 my $end= $span->scan("\n-----END $label_str-----");
27 9 50       31 unless ($end) {
28 0         0 carp "PEM begin marker for $label_str lacks an END marker";
29 0         0 next;
30             }
31 9         80 $end->ltrim("\n");
32 9         39 $span->pos($end->lim);
33 9         51 $span->ltrim("\r");
34 9         30 $span->ltrim("\n"); # consume line ending
35              
36             # Let block be its own SecretBuffer
37 9         110 my $block= $span->clone(pos => $begin->pos, lim => $span->pos)->copy;
38 9         98 my $inner= $block->span(pos => $begin->len, lim => $end->pos - $begin->pos);
39 9 50       88 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         24 my @headers;
45 9         85 while (my $sep_or_eol= $inner->scan(qr/[:\n]/)) {
46 13 100       60 if ($sep_or_eol->starts_with(':')) {
47 5         30 my ($name, $value);
48 5         34 my $name_span= $inner->clone(lim => $sep_or_eol->pos);
49 5 50       91 $name_span->trim unless $notrim;
50 5         45 $name_span->copy_to($name);
51 5         23 $inner->pos($sep_or_eol->lim);
52 5 50       34 my $eol= $inner->scan("\n") or die "BUG"; # inner ends with "\n", checked above
53 5         29 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       35 $notrim? $val_span->ltrim(' ') : $val_span->trim;
57 5         20 $inner->pos($eol->lim);
58 5 100       12 if ($secret_headers) {
59 2         27 push @headers, $name, $val_span;
60             } else {
61 3         17 $val_span->copy_to($value);
62 3         42 push @headers, $name, $value;
63             }
64             }
65             else {
66             # If any headers were found, there needs to be a blank line
67 8 100       18 if (@headers) {
68 3 50       19 if ($sep_or_eol->pos == $inner->pos) { # "\n" at start of 'inner'
69 3         47 $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         23 last;
75             }
76             }
77 9         111 $inner->encoding(BASE64);
78 9         37 return $class->new(
79             buffer => $block,
80             label => $label_str,
81             header_kv => \@headers,
82             content => $inner,
83             );
84             }
85 1         5 return undef;
86             }
87              
88             sub parse_all {
89 1     1 1 3105 my ($class, $span, %options)= @_;
90 1         2 my @pem;
91 1         6 while (my $pem= $class->parse($span, %options)) {
92 3         12 push @pem, $pem;
93             }
94 1         6 return @pem;
95             }
96              
97             sub new {
98 10     10 1 4718 my $class= shift;
99 10         31 my $self= bless {}, $class;
100 10         31 while (@_) {
101 38         101 my ($attr, $val)= splice(@_, 0, 2);
102 38         135 $self->$attr($val);
103             }
104 10         98 $self;
105             }
106              
107              
108 9 50   9 1 42 sub buffer { $_[0]{buffer}= $_[1] if @_ > 1; $_[0]{buffer} }
  9         27  
109 30 100   30 1 9218 sub label { $_[0]{label}= $_[1] if @_ > 1; $_[0]{label} }
  30         160  
110 23 100   23 1 3033 sub content { $_[0]{content}= $_[1] if @_ > 1; $_[0]{content} }
  23         92  
111             sub header_kv {
112 25 100   25 1 1291 if (@_ > 1) {
113 10         31 _validate_header_kv($_[1]);
114 10         27 $_[0]{header_kv}= $_[1];
115             $_[0]{headers}->raw_kv_array($_[1])
116 10 50       81 if defined $_[0]{headers};
117             }
118             $_[0]{header_kv}
119 25         98 }
120             sub headers {
121 6     6 1 3381 my $self= shift;
122 6         55 require Crypt::SecretBuffer::PEM::Headers;
123             $self->{headers} ||=
124 6   66     41 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   27 my $kv= shift;
131 13 50 33     97 croak "Expected even-length arrayref"
132             unless ref $kv eq 'ARRAY' && ($#$kv & 1);
133 13         61 for (0..($#$kv-1)/2) {
134 13         63 my ($k, $v)= ($kv->[$_*2], $kv->[$_*2+1]);
135 13 50       36 croak "PEM header Key is undefined"
136             unless defined $k;
137 13 50       49 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       84 croak "PEM Header name '$k' contains ':' or control characters"
141             if $k =~ /[\0-\x1F\x7F:]/;
142 13 100 66     375 carp "PEM header name '$k' contains leading/trailing whitespace"
143             if $k =~ /^\s/ or $k =~ /\s\z/;
144 13 50       37 croak "PEM header value for '$k' is undefined"
145             unless defined $v;
146 13   66     46 my $is_secret= blessed($v) && ($v->isa('Crypt::SecretBuffer::Span') || $v->isa('Crypt::SecretBuffer'));
147 13 50 66     59 croak "PEM header value for $k' contains wide characters"
148             unless $is_secret || utf8::downgrade($v, 1);
149 13 100       60 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     250 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         28 1;
157             }
158              
159              
160             sub serialize {
161 6     6 1 4917 my $self= shift;
162 6         18 my $out= secret('-----BEGIN '.$self->label."-----\n");
163 6 50       14 my @header_kv= @{ $self->header_kv || [] };
  6         20  
164 6 100       19 if (@header_kv) {
165             # re-validate since individual values are mutable and may have changed since the
166             # attribute was assigned.
167 3         12 _validate_header_kv(\@header_kv);
168 3         11 for (0..$#header_kv) {
169 10 100       58 $out->append($header_kv[$_])->append($_ & 1? "\n" : ": ");
170             }
171 3         10 $out->append("\n"); # empty line terminates headers
172             }
173 6         16 my $content_span= span($self->content);
174 6         57 $content_span->append_to($out, encoding => BASE64);
175 6 100       31 $out->append(($content_span->length? "\n" : '')
176             .'-----END '.$self->label."-----\n");
177 6         69 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__