8b9673ae114878484e6a861ebe05fe46d449ac62
[oweals/luci.git] / modules / luci-mod-system / htdocs / luci-static / resources / view / system / sshkeys.js
1 'use strict';
2 'require fs';
3 'require ui';
4
5 var SSHPubkeyDecoder = L.Class.singleton({
6         lengthDecode: function(s, off)
7         {
8                 var l = (s.charCodeAt(off++) << 24) |
9                                 (s.charCodeAt(off++) << 16) |
10                                 (s.charCodeAt(off++) <<  8) |
11                                  s.charCodeAt(off++);
12
13                 if (l < 0 || (off + l) > s.length)
14                         return -1;
15
16                 return l;
17         },
18
19         decode: function(s)
20         {
21                 var parts = s.split(/\s+/);
22                 if (parts.length < 2)
23                         return null;
24
25                 var key = null;
26                 try { key = atob(parts[1]); } catch(e) {}
27                 if (!key)
28                         return null;
29
30                 var off, len;
31
32                 off = 0;
33                 len = this.lengthDecode(key, off);
34
35                 if (len <= 0)
36                         return null;
37
38                 var type = key.substr(off + 4, len);
39                 if (type !== parts[0])
40                         return null;
41
42                 off += 4 + len;
43
44                 var len1 = off < key.length ? this.lengthDecode(key, off) : 0;
45                 if (len1 <= 0)
46                         return null;
47
48                 var curve = null;
49                 if (type.indexOf('ecdsa-sha2-') === 0) {
50                         curve = key.substr(off + 4, len1);
51
52                         if (!len1 || type.substr(11) !== curve)
53                                 return null;
54
55                         type = 'ecdsa-sha2';
56                         curve = curve.replace(/^nistp(\d+)$/, 'NIST P-$1');
57                 }
58
59                 off += 4 + len1;
60
61                 var len2 = off < key.length ? this.lengthDecode(key, off) : 0;
62                 if (len2 < 0)
63                         return null;
64
65                 if (len1 & 1)
66                         len1--;
67
68                 if (len2 & 1)
69                         len2--;
70
71                 var comment = parts.slice(2).join(' '),
72                     fprint = parts[1].length > 68 ? parts[1].substr(0, 33) + '…' + parts[1].substr(-34) : parts[1];
73
74                 switch (type)
75                 {
76                 case 'ssh-rsa':
77                         return { type: 'RSA', bits: len2 * 8, comment: comment, fprint: fprint };
78
79                 case 'ssh-dss':
80                         return { type: 'DSA', bits: len1 * 8, comment: comment, fprint: fprint };
81
82                 case 'ssh-ed25519':
83                         return { type: 'ECDH', curve: 'Curve25519', comment: comment, fprint: fprint };
84
85                 case 'ecdsa-sha2':
86                         return { type: 'ECDSA', curve: curve, comment: comment, fprint: fprint };
87
88                 default:
89                         return null;
90                 }
91         }
92 });
93
94 function renderKeys(keys) {
95         var list = document.querySelector('.cbi-dynlist');
96
97         while (!matchesElem(list.firstElementChild, '.add-item'))
98                 list.removeChild(list.firstElementChild);
99
100         keys.forEach(function(key) {
101                 var pubkey = SSHPubkeyDecoder.decode(key);
102                 if (pubkey)
103                         list.insertBefore(E('div', {
104                                 class: 'item',
105                                 click: removeKey,
106                                 'data-key': key
107                         }, [
108                                 E('strong', pubkey.comment || _('Unnamed key')), E('br'),
109                                 E('small', [
110                                         '%s, %s'.format(pubkey.type, pubkey.curve || _('%d Bit').format(pubkey.bits)),
111                                         E('br'), E('code', pubkey.fprint)
112                                 ])
113                         ]), list.lastElementChild);
114         });
115
116         if (list.firstElementChild === list.lastElementChild)
117                 list.insertBefore(E('p', _('No public keys present yet.')), list.lastElementChild);
118 }
119
120 function saveKeys(keys) {
121         return fs.write('/etc/dropbear/authorized_keys', keys.join('\n') + '\n', 384 /* 0600 */)
122                 .then(renderKeys.bind(this, keys))
123                 .catch(function(e) { ui.addNotification(null, E('p', e.message)) })
124                 .finally(ui.hideModal);
125 }
126
127 function addKey(ev) {
128         var list = findParent(ev.target, '.cbi-dynlist'),
129             input = list.querySelector('input[type="text"]'),
130             key = input.value.trim(),
131             pubkey = SSHPubkeyDecoder.decode(key),
132             keys = [];
133
134         if (!key.length)
135                 return;
136
137         list.querySelectorAll('.item').forEach(function(item) {
138                 keys.push(item.getAttribute('data-key'));
139         });
140
141         if (keys.indexOf(key) !== -1) {
142                 ui.showModal(_('Add key'), [
143                         E('div', { class: 'alert-message warning' }, _('The given SSH public key has already been added.')),
144                         E('div', { class: 'right' }, E('div', { class: 'btn', click: L.hideModal }, _('Close')))
145                 ]);
146         }
147         else if (!pubkey) {
148                 ui.showModal(_('Add key'), [
149                         E('div', { class: 'alert-message warning' }, _('The given SSH public key is invalid. Please supply proper public RSA or ECDSA keys.')),
150                         E('div', { class: 'right' }, E('div', { class: 'btn', click: L.hideModal }, _('Close')))
151                 ]);
152         }
153         else {
154                 keys.push(key);
155                 input.value = '';
156
157                 return saveKeys(keys).then(function() {
158                         var added = list.querySelector('[data-key="%s"]'.format(key));
159                         if (added)
160                                 added.classList.add('flash');
161                 });
162         }
163 }
164
165 function removeKey(ev) {
166         var list = findParent(ev.target, '.cbi-dynlist'),
167             delkey = ev.target.getAttribute('data-key'),
168             keys = [];
169
170         list.querySelectorAll('.item').forEach(function(item) {
171                 var key = item.getAttribute('data-key');
172                 if (key !== delkey)
173                         keys.push(key);
174         });
175
176         L.showModal(_('Delete key'), [
177                 E('div', _('Do you really want to delete the following SSH key?')),
178                 E('pre', delkey),
179                 E('div', { class: 'right' }, [
180                         E('div', { class: 'btn', click: L.hideModal }, _('Cancel')),
181                         ' ',
182                         E('div', { class: 'btn danger', click: ui.createHandlerFn(this, saveKeys, keys) }, _('Delete key')),
183                 ])
184         ]);
185 }
186
187 function dragKey(ev) {
188         ev.stopPropagation();
189         ev.preventDefault();
190         ev.dataTransfer.dropEffect = 'copy';
191 }
192
193 function dropKey(ev) {
194         var file = ev.dataTransfer.files[0],
195             input = ev.currentTarget.querySelector('input[type="text"]'),
196             reader = new FileReader();
197
198         if (file) {
199                 reader.onload = function(rev) {
200                         input.value = rev.target.result.trim();
201                         addKey(ev);
202                         input.value = '';
203                 };
204
205                 reader.readAsText(file);
206         }
207
208         ev.stopPropagation();
209         ev.preventDefault();
210 }
211
212 function handleWindowDragDropIgnore(ev) {
213         ev.preventDefault()
214 }
215
216 return L.view.extend({
217         load: function() {
218                 return fs.lines('/etc/dropbear/authorized_keys').then(function(lines) {
219                         return lines.filter(function(line) {
220                                 return line.match(/^ssh-/) != null;
221                         });
222                 });
223         },
224
225         render: function(keys) {
226                 var list = E('div', { 'class': 'cbi-dynlist', 'dragover': dragKey, 'drop': dropKey }, [
227                         E('div', { 'class': 'add-item' }, [
228                                 E('input', {
229                                         'class': 'cbi-input-text',
230                                         'type': 'text',
231                                         'placeholder': _('Paste or drag SSH key file…') ,
232                                         'keydown': function(ev) { if (ev.keyCode === 13) addKey(ev) }
233                                 }),
234                                 E('button', {
235                                         'class': 'cbi-button',
236                                         'click': ui.createHandlerFn(this, addKey)
237                                 }, _('Add key'))
238                         ])
239                 ]);
240
241                 keys.forEach(L.bind(function(key) {
242                         var pubkey = SSHPubkeyDecoder.decode(key);
243                         if (pubkey)
244                                 list.insertBefore(E('div', {
245                                         class: 'item',
246                                         click: ui.createHandlerFn(this, removeKey),
247                                         'data-key': key
248                                 }, [
249                                         E('strong', pubkey.comment || _('Unnamed key')), E('br'),
250                                         E('small', [
251                                                 '%s, %s'.format(pubkey.type, pubkey.curve || _('%d Bit').format(pubkey.bits)),
252                                                 E('br'), E('code', pubkey.fprint)
253                                         ])
254                                 ]), list.lastElementChild);
255                 }, this));
256
257                 if (list.firstElementChild === list.lastElementChild)
258                         list.insertBefore(E('p', _('No public keys present yet.')), list.lastElementChild);
259
260                 window.addEventListener('dragover', handleWindowDragDropIgnore);
261                 window.addEventListener('drop', handleWindowDragDropIgnore);
262
263                 return E('div', {}, [
264                         E('h2', _('SSH-Keys')),
265                         E('div', { 'class': 'cbi-section-descr' }, _('Public keys allow for the passwordless SSH logins with a higher security compared to the use of plain passwords. In order to upload a new key to the device, paste an OpenSSH compatible public key line or drag a <code>.pub</code> file into the input field.')),
266                         E('div', { 'class': 'cbi-section-node' }, list)
267                 ]);
268         },
269
270         handleSaveApply: null,
271         handleSave: null,
272         handleReset: null
273 });