gcr: Implement OpenSSH public key parser
[platform/upstream/gcr.git] / gcr / gcr-openssh.c
1 /*
2  * gnome-keyring
3  *
4  * Copyright (C) 2011 Collabora Ltd.
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU Lesser General Public License as
8  * published by the Free Software Foundation; either version 2.1 of
9  * the License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful, but
12  * WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * Lesser General Public License for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public
17  * License along with this program; if not, write to the Free Software
18  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
19  * 02111-1307, USA.
20  *
21  * Author: Stef Walter <stefw@collabora.co.uk>
22  */
23
24 #include "config.h"
25
26 #include "gcr-openssh.h"
27 #include "gcr-internal.h"
28 #include "gcr-types.h"
29
30 #include "egg/egg-buffer.h"
31 #include "egg/egg-decimal.h"
32
33 #include "pkcs11/pkcs11.h"
34
35 #include <string.h>
36
37 typedef struct {
38         GcrOpensshPubCallback callback;
39         gpointer user_data;
40 } OpensshPubClosure;
41
42 static void
43 skip_spaces (const gchar ** line,
44              gsize *n_line)
45 {
46         while (*n_line > 0 && (*line)[0] == ' ') {
47                 (*line)++;
48                 (*n_line)--;
49         }
50 }
51
52 static gboolean
53 next_word (const gchar **line,
54            gsize *n_line,
55            const gchar **word,
56            gsize *n_word)
57 {
58         const gchar *beg;
59         const gchar *end;
60         const gchar *at;
61         gboolean quotes;
62
63         skip_spaces (line, n_line);
64
65         if (!*n_line) {
66                 *word = NULL;
67                 *n_word = 0;
68                 return FALSE;
69         }
70
71         beg = at = *line;
72         end = beg + *n_line;
73         quotes = FALSE;
74
75         do {
76                 switch (*at) {
77                 case '"':
78                         quotes = !quotes;
79                         at++;
80                         break;
81                 case ' ':
82                         if (!quotes)
83                                 end = at;
84                         else
85                                 at++;
86                         break;
87                 default:
88                         at++;
89                         break;
90                 }
91         } while (at < end);
92
93         *word = beg;
94         *n_word = end - beg;
95         (*line) += *n_word;
96         (*n_line) -= *n_word;
97         return TRUE;
98 }
99
100 static gboolean
101 match_word (const gchar *word,
102             gsize n_word,
103             const gchar *matches)
104 {
105         gsize len = strlen (matches);
106         if (len != n_word)
107                 return FALSE;
108         return memcmp (word, matches, n_word) == 0;
109 }
110
111 static gulong
112 keytype_to_algo (const gchar *algo,
113                  gsize length)
114 {
115         if (!algo)
116                 return G_MAXULONG;
117         else if (match_word (algo, length, "ssh-rsa"))
118                 return CKK_RSA;
119         else if (match_word (algo, length, "ssh-dss"))
120                 return CKK_DSA;
121         return G_MAXULONG;
122 }
123
124 static gboolean
125 read_decimal_mpi (const gchar *decimal,
126                   gsize n_decimal,
127                   GckAttributes *attrs,
128                   gulong attribute_type)
129 {
130         gpointer data;
131         gsize n_data;
132
133         data = egg_decimal_decode (decimal, n_decimal, &n_data);
134         if (data == NULL)
135                 return FALSE;
136
137         gck_attributes_add_data (attrs, attribute_type, data, n_data);
138         return TRUE;
139 }
140
141 static gint
142 atoin (const char *p, gint digits)
143 {
144         gint ret = 0, base = 1;
145         while(--digits >= 0) {
146                 if (p[digits] < '0' || p[digits] > '9')
147                         return -1;
148                 ret += (p[digits] - '0') * base;
149                 base *= 10;
150         }
151         return ret;
152 }
153
154 static GcrDataError
155 parse_v1_public_line (const gchar *line,
156                       gsize length,
157                       GcrOpensshPubCallback callback,
158                       gpointer user_data)
159 {
160         const gchar *word_bits, *word_exponent, *word_modulus, *word_options, *outer;
161         gsize len_bits, len_exponent, len_modulus, len_options, n_outer;
162         GckAttributes *attrs;
163         gchar *label, *options;
164         gint bits;
165
166         g_assert (line);
167
168         outer = line;
169         n_outer = length;
170         options = NULL;
171         label = NULL;
172
173         /* Eat space at the front */
174         skip_spaces (&line, &length);
175
176         /* Blank line or comment */
177         if (length == 0 || line[0] == '#')
178                 return GCR_ERROR_UNRECOGNIZED;
179
180         /*
181          * If the line starts with a digit, then no options:
182          *
183          * 2048 35 25213680043....93533757 Label
184          *
185          * If the line doesn't start with a digit, then have options:
186          *
187          * option,option 2048 35 25213680043....93533757 Label
188          */
189         if (g_ascii_isdigit (line[0])) {
190                 word_options = NULL;
191                 len_options = 0;
192         } else {
193                 if (!next_word (&line, &length, &word_options, &len_options))
194                         return GCR_ERROR_UNRECOGNIZED;
195         }
196
197         if (!next_word (&line, &length, &word_bits, &len_bits) ||
198             !next_word (&line, &length, &word_exponent, &len_exponent) ||
199             !next_word (&line, &length, &word_modulus, &len_modulus))
200                 return GCR_ERROR_UNRECOGNIZED;
201
202         bits = atoin (word_bits, len_bits);
203         if (bits <= 0)
204                 return GCR_ERROR_UNRECOGNIZED;
205
206         attrs = gck_attributes_new ();
207
208         if (!read_decimal_mpi (word_exponent, len_exponent, attrs, CKA_PUBLIC_EXPONENT) ||
209             !read_decimal_mpi (word_modulus, len_modulus, attrs, CKA_MODULUS)) {
210                 gck_attributes_unref (attrs);
211                 return GCR_ERROR_UNRECOGNIZED;
212         }
213
214         gck_attributes_add_ulong (attrs, CKA_KEY_TYPE, CKK_RSA);
215         gck_attributes_add_ulong (attrs, CKA_CLASS, CKO_PUBLIC_KEY);
216
217         skip_spaces (&line, &length);
218         if (length > 0) {
219                 label = g_strndup (line, length);
220                 g_strstrip (label);
221                 gck_attributes_add_string (attrs, CKA_LABEL, label);
222         }
223
224         if (word_options)
225                 options = g_strndup (word_options, len_options);
226
227         if (callback != NULL)
228                 (callback) (attrs, label, options, outer, n_outer, user_data);
229
230         gck_attributes_unref (attrs);
231         g_free (options);
232         g_free (label);
233         return GCR_SUCCESS;
234 }
235
236 static gboolean
237 read_buffer_mpi (EggBuffer *buffer,
238                  gsize *offset,
239                  GckAttributes *attrs,
240                  gulong attribute_type)
241 {
242         const guchar *data;
243         gsize len;
244
245         if (!egg_buffer_get_byte_array (buffer, *offset, offset, &data, &len))
246                 return FALSE;
247
248         gck_attributes_add_data (attrs, attribute_type, data, len);
249         return TRUE;
250 }
251
252 static GckAttributes *
253 read_v2_public_dsa (EggBuffer *buffer,
254                     gsize *offset)
255 {
256         GckAttributes *attrs;
257
258         attrs = gck_attributes_new ();
259
260         if (!read_buffer_mpi (buffer, offset, attrs, CKA_PRIME) ||
261             !read_buffer_mpi (buffer, offset, attrs, CKA_SUBPRIME) ||
262             !read_buffer_mpi (buffer, offset, attrs, CKA_BASE) ||
263             !read_buffer_mpi (buffer, offset, attrs, CKA_VALUE)) {
264                 gck_attributes_unref (attrs);
265                 return NULL;
266         }
267
268         gck_attributes_add_ulong (attrs, CKA_KEY_TYPE, CKK_DSA);
269         gck_attributes_add_ulong (attrs, CKA_CLASS, CKO_PUBLIC_KEY);
270
271         return attrs;
272 }
273
274 static GckAttributes *
275 read_v2_public_rsa (EggBuffer *buffer,
276                     gsize *offset)
277 {
278         GckAttributes *attrs;
279
280         attrs = gck_attributes_new ();
281
282         if (!read_buffer_mpi (buffer, offset, attrs, CKA_PUBLIC_EXPONENT) ||
283             !read_buffer_mpi (buffer, offset, attrs, CKA_MODULUS)) {
284                 gck_attributes_unref (attrs);
285                 return NULL;
286         }
287
288         gck_attributes_add_ulong (attrs, CKA_KEY_TYPE, CKK_RSA);
289         gck_attributes_add_ulong (attrs, CKA_CLASS, CKO_PUBLIC_KEY);
290
291         return attrs;
292 }
293
294 static GckAttributes *
295 read_v2_public_key (gulong algo,
296                     gconstpointer data,
297                     gsize n_data)
298 {
299         GckAttributes *attrs;
300         EggBuffer buffer;
301         gsize offset;
302         gchar *stype;
303         int alg;
304
305         egg_buffer_init_static (&buffer, data, n_data);
306         offset = 0;
307
308         /* The string algorithm */
309         if (!egg_buffer_get_string (&buffer, offset, &offset,
310                                     &stype, (EggBufferAllocator)g_realloc))
311                 return NULL;
312
313         alg = keytype_to_algo (stype, stype ? strlen (stype) : 0);
314         g_free (stype);
315
316         if (alg != algo) {
317                 g_message ("invalid or mis-matched algorithm in ssh public key: %s", stype);
318                 egg_buffer_uninit (&buffer);
319                 return NULL;
320         }
321
322         switch (algo) {
323         case CKK_RSA:
324                 attrs = read_v2_public_rsa (&buffer, &offset);
325                 break;
326         case CKK_DSA:
327                 attrs = read_v2_public_dsa (&buffer, &offset);
328                 break;
329         default:
330                 g_assert_not_reached ();
331                 break;
332         }
333
334         egg_buffer_uninit (&buffer);
335         return attrs;
336 }
337
338 static GckAttributes *
339 decode_v2_public_key (gulong algo,
340                       const gchar *data,
341                       gsize n_data)
342 {
343         GckAttributes *attrs;
344         gpointer decoded;
345         gsize n_decoded;
346         guint save;
347         gint state;
348
349         /* Decode the base64 key */
350         save = state = 0;
351         decoded = g_malloc (n_data * 3 / 4);
352         n_decoded = g_base64_decode_step ((gchar*)data, n_data, decoded, &state, &save);
353
354         if (!n_decoded) {
355                 g_free (decoded);
356                 return NULL;
357         }
358
359         /* Parse the actual key */
360         attrs = read_v2_public_key (algo, decoded, n_decoded);
361
362         g_free (decoded);
363
364         return attrs;
365 }
366
367 static GcrDataError
368 parse_v2_public_line (const gchar *line,
369                       gsize length,
370                       GcrOpensshPubCallback callback,
371                       gpointer user_data)
372 {
373         const gchar *word_options, *word_algo, *word_key;
374         gsize len_options, len_algo, len_key;
375         GckAttributes *attrs;
376         gchar *options;
377         gchar *label = NULL;
378         const gchar *outer = line;
379         gsize n_outer = length;
380         gulong algo;
381
382         g_assert (line);
383
384         /* Eat space at the front */
385         skip_spaces (&line, &length);
386
387         /* Blank line or comment */
388         if (length == 0 || line[0] == '#')
389                 return GCR_ERROR_UNRECOGNIZED;
390
391         if (!next_word (&line, &length, &word_algo, &len_algo))
392                 return GCR_ERROR_UNRECOGNIZED;
393
394         /*
395          * If the first word is not the algorithm, then we have options:
396          *
397          * option,option ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAI...EAz8Ji= Label here
398          *
399          * If the first word is the algorithm, then we have no options:
400          *
401          * ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAI...EAz8Ji= Label here
402          */
403         algo = keytype_to_algo (word_algo, len_algo);
404         if (algo == G_MAXULONG) {
405                 word_options = word_algo;
406                 len_options = len_algo;
407                 if (!next_word (&line, &length, &word_algo, &len_algo))
408                         return GCR_ERROR_UNRECOGNIZED;
409                 algo = keytype_to_algo (word_algo, len_algo);
410                 if (algo == G_MAXULONG)
411                         return GCR_ERROR_UNRECOGNIZED;
412         } else {
413                 word_options = NULL;
414                 len_options = 0;
415         }
416
417         /* Must have at least two words */
418         if (!next_word (&line, &length, &word_key, &len_key))
419                 return GCR_ERROR_FAILURE;
420
421         attrs = decode_v2_public_key (algo, word_key, len_key);
422         if (attrs == NULL)
423                 return GCR_ERROR_FAILURE;
424
425         if (word_options)
426                 options = g_strndup (word_options, len_options);
427         else
428                 options = NULL;
429
430         /* The remainder of the line is the label */
431         skip_spaces (&line, &length);
432         if (length > 0) {
433                 label = g_strndup (line, length);
434                 g_strstrip (label);
435                 gck_attributes_add_string (attrs, CKA_LABEL, label);
436         }
437
438         if (callback != NULL)
439                 (callback) (attrs, label, options, outer, n_outer, user_data);
440
441         gck_attributes_unref (attrs);
442         g_free (options);
443         g_free (label);
444         return GCR_SUCCESS;
445 }
446
447 guint
448 _gcr_openssh_pub_parse (gconstpointer data,
449                         gsize n_data,
450                         GcrOpensshPubCallback callback,
451                         gpointer user_data)
452 {
453         const gchar *line;
454         const gchar *end;
455         gsize length;
456         gboolean last;
457         GcrDataError res;
458         guint num_parsed;
459
460         g_return_val_if_fail (data, FALSE);
461
462         line = data;
463         length = n_data;
464         last = FALSE;
465         num_parsed = 0;
466
467         for (;;) {
468                 end  = memchr (line, '\n', length);
469                 if (end == NULL) {
470                         end = line + length;
471                         last = TRUE;
472                 }
473
474                 if (line != end) {
475                         res = parse_v2_public_line (line, end - line, callback, user_data);
476                         if (res == GCR_ERROR_UNRECOGNIZED)
477                                 res = parse_v1_public_line (line, end - line, callback, user_data);
478                         if (res == GCR_SUCCESS)
479                                 num_parsed++;
480                 }
481
482                 if (last)
483                         break;
484
485                 end++;
486                 length -= (end - line);
487                 line = end;
488         }
489
490         return num_parsed;
491 }