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