luci-mod-system: fix parsing SSH pubkeys with options
authorJo-Philipp Wich <jo@mein.io>
Wed, 23 Dec 2020 14:31:58 +0000 (15:31 +0100)
committerJo-Philipp Wich <jo@mein.io>
Wed, 23 Dec 2020 14:35:26 +0000 (15:35 +0100)
Also eliminate some duplicate code while we're at it.

Fixes: #4684
Signed-off-by: Jo-Philipp Wich <jo@mein.io>
(backported from commit 846b89c5bf07100f9bf1e46bcac55acab5535e26)

modules/luci-mod-system/htdocs/luci-static/resources/view/system/sshkeys.js

index 487621daa32de40e3419c90d8113a3d920b5c8f9..c0edbf45045f6e0837fea7ba9072c611dc9a73b1 100644 (file)
@@ -19,12 +19,13 @@ var SSHPubkeyDecoder = L.Class.singleton({
 
        decode: function(s)
        {
-               var parts = s.split(/\s+/);
-               if (parts.length < 2)
+               var parts = s.trim().match(/^((?:(?:^|,)[^ =,]+(?:=(?:[^ ",]+|"(?:[^"\\]|\\.)*"))?)+ +)?(ssh-dss|ssh-rsa|ssh-ed25519|ecdsa-sha2-nistp[0-9]+) +([^ ]+)( +.*)?$/);
+
+               if (!parts)
                        return null;
 
                var key = null;
-               try { key = atob(parts[1]); } catch(e) {}
+               try { key = atob(parts[3]); } catch(e) {}
                if (!key)
                        return null;
 
@@ -37,7 +38,7 @@ var SSHPubkeyDecoder = L.Class.singleton({
                        return null;
 
                var type = key.substr(off + 4, len);
-               if (type !== parts[0])
+               if (type !== parts[2])
                        return null;
 
                off += 4 + len;
@@ -69,22 +70,32 @@ var SSHPubkeyDecoder = L.Class.singleton({
                if (len2 & 1)
                        len2--;
 
-               var comment = parts.slice(2).join(' '),
-                   fprint = parts[1].length > 68 ? parts[1].substr(0, 33) + '…' + parts[1].substr(-34) : parts[1];
+               var comment = (parts[4] || '').trim(),
+                   fprint = parts[3].length > 68 ? parts[3].substr(0, 33) + '…' + parts[3].substr(-34) : parts[3];
+
+               var options = null;
+               (parts[1] || '').trim().replace(/(?:^|,)([^ =,]+)(?:=(?:([^ ",]+)|"((?:[^"\\]|\\.)*)"))?/g, function(m, k, p, q) {
+                       options = options || {};
+
+                       if (options.hasOwnProperty(k))
+                               options[k] += ',' + (q || p || true);
+                       else
+                               options[k] = (q || p || true);
+               });
 
                switch (type)
                {
                case 'ssh-rsa':
-                       return { type: 'RSA', bits: len2 * 8, comment: comment, fprint: fprint };
+                       return { type: 'RSA', bits: len2 * 8, comment: comment, options: options, fprint: fprint, src: s };
 
                case 'ssh-dss':
-                       return { type: 'DSA', bits: len1 * 8, comment: comment, fprint: fprint };
+                       return { type: 'DSA', bits: len1 * 8, comment: comment, options: options, fprint: fprint, src: s };
 
                case 'ssh-ed25519':
-                       return { type: 'ECDH', curve: 'Curve25519', comment: comment, fprint: fprint };
+                       return { type: 'ECDH', curve: 'Curve25519', comment: comment, options: options, fprint: fprint, src: s };
 
                case 'ecdsa-sha2':
-                       return { type: 'ECDSA', curve: curve, comment: comment, fprint: fprint };
+                       return { type: 'ECDSA', curve: curve, comment: comment, options: options, fprint: fprint, src: s };
 
                default:
                        return null;
@@ -92,6 +103,24 @@ var SSHPubkeyDecoder = L.Class.singleton({
        }
 });
 
+function renderKeyItem(pubkey) {
+       return E('div', {
+               class: 'item',
+               click: isReadonlyView ? null : removeKey,
+               'data-key': pubkey.src
+       }, [
+               E('strong', pubkey.comment || _('Unnamed key')), E('br'),
+               E('small', [
+                       '%s, %s'.format(pubkey.type, pubkey.curve || _('%d Bit').format(pubkey.bits)),
+                       pubkey.options ? E([], [
+                               ' / ', _('Options:'), ' ',
+                               E('code', Object.keys(pubkey.options).sort().join(', '))
+                       ]) : '',
+                       E('br'), E('code', pubkey.fprint)
+               ])
+       ]);
+}
+
 function renderKeys(keys) {
        var list = document.querySelector('.cbi-dynlist');
 
@@ -101,17 +130,7 @@ function renderKeys(keys) {
        keys.forEach(function(key) {
                var pubkey = SSHPubkeyDecoder.decode(key);
                if (pubkey)
-                       list.insertBefore(E('div', {
-                               class: 'item',
-                               click: removeKey,
-                               'data-key': key
-                       }, [
-                               E('strong', pubkey.comment || _('Unnamed key')), E('br'),
-                               E('small', [
-                                       '%s, %s'.format(pubkey.type, pubkey.curve || _('%d Bit').format(pubkey.bits)),
-                                       E('br'), E('code', pubkey.fprint)
-                               ])
-                       ]), list.lastElementChild);
+                       list.insertBefore(renderKeyItem(pubkey), list.lastElementChild);
        });
 
        if (list.firstElementChild === list.lastElementChild)
@@ -156,7 +175,7 @@ function addKey(ev) {
                input.value = '';
 
                return saveKeys(keys).then(function() {
-                       var added = list.querySelector('[data-key="%s"]'.format(key));
+                       var added = list.querySelector('[data-key="%s"]'.format(key.replace(/["\\]/g, '\\$&')));
                        if (added)
                                added.classList.add('flash');
                });
@@ -217,8 +236,10 @@ function handleWindowDragDropIgnore(ev) {
 return view.extend({
        load: function() {
                return fs.lines('/etc/dropbear/authorized_keys').then(function(lines) {
-                       return lines.filter(function(line) {
-                               return line.match(/^(ssh-rsa|ssh-dss|ssh-ed25519|ecdsa-sha2)\b/) != null;
+                       return lines.map(function(line) {
+                               return SSHPubkeyDecoder.decode(line);
+                       }).filter(function(line) {
+                               return line != null;
                        });
                });
        },
@@ -239,20 +260,8 @@ return view.extend({
                        ])
                ]);
 
-               keys.forEach(L.bind(function(key) {
-                       var pubkey = SSHPubkeyDecoder.decode(key);
-                       if (pubkey)
-                               list.insertBefore(E('div', {
-                                       class: 'item',
-                                       click: ui.createHandlerFn(this, removeKey),
-                                       'data-key': key
-                               }, [
-                                       E('strong', pubkey.comment || _('Unnamed key')), E('br'),
-                                       E('small', [
-                                               '%s, %s'.format(pubkey.type, pubkey.curve || _('%d Bit').format(pubkey.bits)),
-                                               E('br'), E('code', pubkey.fprint)
-                                       ])
-                               ]), list.lastElementChild);
+               keys.forEach(L.bind(function(pubkey) {
+                       list.insertBefore(renderKeyItem(pubkey), list.lastElementChild);
                }, this));
 
                if (list.firstElementChild === list.lastElementChild)