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