File Coverage

blib/lib/Config/OpenSSH/Authkey/Entry.pm
Criterion Covered Total %
statement 87 92 94.5
branch 35 42 83.3
condition 7 15 46.6
subroutine 21 21 100.0
pod 16 16 100.0
total 166 186 89.2


line stmt bran cond sub pod time code
1             # -*- Perl -*-
2             #
3             # representation of individual OpenSSH authorized_keys entries, based on
4             # a study of the sshd(8) manual, along with the OpenSSH 5.2 sources.
5             # this module only weakly validates the data; in particular, no effort
6             # is made to confirm whether the key options are actual valid options
7             # for the version of OpenSSH in question
8              
9             package Config::OpenSSH::Authkey::Entry;
10              
11 2     2   68193 use 5.006000;
  2         7  
12 2     2   11 use strict;
  2         5  
  2         41  
13 2     2   8 use warnings;
  2         3  
  2         65  
14              
15 2     2   934 use Config::OpenSSH::Authkey::Entry::Options ();
  2         6  
  2         48  
16              
17 2     2   20 use Carp qw/croak/;
  2         3  
  2         3058  
18              
19             our $VERSION = '1.06';
20              
21             # This limit is set for various things under OpenSSH code. Used here to
22             # limit length of authorized_keys lines.
23             my $MAX_PUBKEY_BYTES = 8192;
24              
25             # Sanity check to ensure at least some data exists in the key field
26             my $MIN_KEY_LENGTH = 42;
27              
28             ######################################################################
29             #
30             # Data Parsing & Utility Methods - Internal
31              
32             my $_parse_entry = sub {
33             my $self = shift;
34             my $data = shift || q{};
35              
36             my ( $options, $key, $comment, $protocol, $keytype );
37              
38             chomp $data;
39              
40             if ( $data =~ m/^\s*$/ or $data =~ m/^\s*#/ ) {
41             return ( 0, 'no public key data' );
42             } elsif ( length $data >= $MAX_PUBKEY_BYTES ) {
43             return ( 0, 'exceeds size limit' );
44             }
45              
46             # OpenSSH supports leading whitespace before options or key. Strip
47             # this optional whitespace to simplify parsing.
48             $data =~ s/^[ \t]+//;
49              
50             ENTRY_LEXER: {
51             # Optional trailing comment (user@host, usually)
52             if ( defined $key and $data =~ m/ \G (.+) /cgx ) {
53             $comment = $1;
54              
55             last ENTRY_LEXER;
56             }
57              
58             # SSH2 public keys
59             if ( !defined $key
60             and $data =~
61             m/ \G ( (ssh-(rsa|dss|ed25519)|ecdsa-sha2-nistp256) [ \t]+? [A-Za-z0-9+\/]+ =* ) [ \t]* /cgx
62             ) {
63              
64             $key = $1;
65             my $type = $2;
66             my $subtype = $3;
67             # follow the -t argument option to ssh-keygen(1)
68             if ( $type =~ m/^ssh-/ ) {
69             if ( $subtype eq 'dss' ) {
70             $keytype = 'dsa';
71             } else {
72             $keytype = $subtype;
73             }
74             } else {
75             $keytype = 'ecdsa';
76             }
77             $protocol = 2;
78              
79             redo ENTRY_LEXER;
80             }
81              
82             # SSH1 RSA public key
83             if ( !defined $key
84             and $data =~ m/ \G ( \d{3,5} [ \t]+? \d+ [ \t]+? \d+ ) [ \t]* /cgx ) {
85              
86             $key = $1;
87             $keytype = 'rsa1';
88             $protocol = 1;
89              
90             redo ENTRY_LEXER;
91             }
92              
93             # Optional leading options - may contain whitespace inside ""
94             if ( !defined $key and $data =~ m/ \G ([^ \t]+? [ \t]*) /cgx ) {
95             $options .= $1;
96              
97             redo ENTRY_LEXER;
98             }
99             }
100              
101             if ( !defined $key ) {
102             return ( 0, 'unable to parse public key' );
103              
104             } else {
105             $self->{_key} = $key;
106             $self->{_protocol} = $protocol;
107             $self->{_keytype} = $keytype;
108              
109             if ( defined $options ) {
110             $options =~ s/\s*$//;
111             $self->{_options} = $options;
112             }
113              
114             if ( defined $comment ) {
115             $comment =~ s/\s*$//;
116             $self->{_comment} = $comment;
117             }
118             }
119              
120             return ( 1, 'ok' );
121             };
122              
123             ######################################################################
124             #
125             # Class methods
126              
127             sub new {
128 28     28 1 6025 my $class = shift;
129 28         44 my $data = shift;
130              
131 28         75 my $self = { _dup_of => 0 };
132              
133 28 100       72 if ( defined $data ) {
134 22         48 my ( $is_parsed, $err_msg ) = $_parse_entry->( $self, $data );
135 22 100       92 if ( !$is_parsed ) {
136 3         516 croak $err_msg;
137             }
138             }
139              
140 25         49 bless $self, $class;
141 25         70 return $self;
142             }
143              
144             sub split_options {
145 1     1 1 644 my $class = shift;
146 1         11 Config::OpenSSH::Authkey::Entry::Options->split_options(@_);
147             }
148              
149             ######################################################################
150             #
151             # Instance methods
152              
153             sub parse {
154 3     3 1 7 my $self = shift;
155 3   33     9 my $data = shift || croak 'no data supplied to parse';
156              
157 3         8 my ( $is_parsed, $err_msg ) = $_parse_entry->( $self, $data );
158 3 50       11 if ( !$is_parsed ) {
159 0         0 croak $err_msg;
160             }
161              
162 3         13 return $self;
163             }
164              
165             sub as_string {
166 19     19 1 40 my $self = shift;
167 19         32 my $string = q{};
168              
169 19 100 66     102 if ( exists $self->{_parsed_options} ) {
    100          
170 1         5 $string .= $self->{_parsed_options}->as_string . q{ };
171              
172             } elsif ( exists $self->{_options} and length $self->{_options} > 0 ) {
173 9         26 $string .= $self->{_options} . q{ };
174             }
175              
176 19 50 33     79 if ( !defined $self->{_key} or length $self->{_key} < $MIN_KEY_LENGTH ) {
177 0         0 croak 'no key material present';
178             }
179 19         42 $string .= $self->{_key};
180              
181 19 100 66     57 if ( exists $self->{_comment} and length $self->{_comment} > 0 ) {
182 9         23 $string .= q{ } . $self->{_comment};
183             }
184              
185 19         101 return $string;
186             }
187              
188             sub key {
189 20     20 1 428 my $self = shift;
190 20         31 my $key = shift;
191 20 100       45 if ( defined $key ) {
192 3         10 my ( $is_parsed, $err_msg ) = $_parse_entry->( $self, $key );
193 3 50       10 if ( !$is_parsed ) {
194 0         0 croak $err_msg;
195             }
196             }
197 20 50 33     84 if ( !defined $self->{_key} or length $self->{_key} < $MIN_KEY_LENGTH ) {
198 0         0 croak 'no key material present';
199             }
200 20         86 return $self->{_key};
201             }
202              
203             sub protocol {
204 3 50   3 1 1120 shift->{_protocol} || 0;
205             }
206              
207             sub keytype {
208 3 50   3 1 22 shift->{_keytype} || '';
209             }
210              
211             sub comment {
212 18     18 1 44 my $self = shift;
213 18         29 my $comment = shift;
214 18 100       43 if ( defined $comment ) {
215 3         8 $self->{_comment} = $comment;
216             }
217 18 100       94 return defined $self->{_comment} ? $self->{_comment} : '';
218             }
219              
220             sub unset_comment {
221 3     3 1 7 my $self = shift;
222 3         6 delete $self->{_comment};
223 3         6 return 1;
224             }
225              
226             # The leading (optional!) options can be dealt with as a string
227             # (options, unset_options), or if parsed, as individual options
228             # (get_option, set_option, unset_option).
229              
230             sub options {
231 26     26 1 871 my $self = shift;
232 26         43 my $new_options = shift;
233              
234 26 100       58 if ( defined $new_options ) {
235 4         17 delete $self->{_parsed_options};
236 4         10 $self->{_options} = $new_options;
237             }
238              
239             my $options_str =
240             exists $self->{_parsed_options}
241             ? $self->{_parsed_options}->as_string
242 26 100       75 : $self->{_options};
243 26 100       123 return defined $options_str ? $options_str : '';
244             }
245              
246             sub unset_options {
247 3     3 1 8 my $self = shift;
248 3         4 delete $self->{_parsed_options};
249 3         6 delete $self->{_options};
250 3         7 return 1;
251             }
252              
253             sub get_option {
254 5     5 1 1094 my $self = shift;
255              
256 5 100       15 if ( !exists $self->{_parsed_options} ) {
257             $self->{_parsed_options} =
258 1         8 Config::OpenSSH::Authkey::Entry::Options->new( $self->{_options} );
259             }
260              
261 5         19 $self->{_parsed_options}->get_option(@_);
262             }
263              
264             sub set_option {
265 4     4 1 8 my $self = shift;
266              
267 4 100       13 if ( !exists $self->{_parsed_options} ) {
268             $self->{_parsed_options} =
269 1         5 Config::OpenSSH::Authkey::Entry::Options->new( $self->{_options} );
270             }
271              
272 4         15 $self->{_parsed_options}->set_option(@_);
273             }
274              
275             sub unset_option {
276 3     3 1 7 my $self = shift;
277              
278 3 50       10 if ( !exists $self->{_parsed_options} ) {
279             $self->{_parsed_options} =
280 0         0 Config::OpenSSH::Authkey::Entry::Options->new( $self->{_options} );
281             }
282              
283 3         10 $self->{_parsed_options}->unset_option(@_);
284             }
285              
286             sub duplicate_of {
287 12     12 1 28 my $self = shift;
288 12         17 my $ref = shift;
289              
290 12 100       32 if ( defined $ref ) {
291 2         5 $self->{_dup_of} = $ref;
292             }
293              
294 12         31 return $self->{_dup_of};
295             }
296              
297             sub unset_duplicate {
298 1     1 1 3 my $self = shift;
299 1         3 $self->{_dup_of} = 0;
300 1         2 return 1;
301             }
302              
303             1;
304              
305             __END__