7 var isReadonlyView = !L.hasViewPermission() || null;
9 var SSHPubkeyDecoder = baseclass.singleton({
10 lengthDecode: function(s, off)
12 var l = (s.charCodeAt(off++) << 24) |
13 (s.charCodeAt(off++) << 16) |
14 (s.charCodeAt(off++) << 8) |
17 if (l < 0 || (off + l) > s.length)
25 var parts = s.split(/\s+/);
30 try { key = atob(parts[1]); } catch(e) {}
37 len = this.lengthDecode(key, off);
42 var type = key.substr(off + 4, len);
43 if (type !== parts[0])
48 var len1 = off < key.length ? this.lengthDecode(key, off) : 0;
53 if (type.indexOf('ecdsa-sha2-') === 0) {
54 curve = key.substr(off + 4, len1);
56 if (!len1 || type.substr(11) !== curve)
60 curve = curve.replace(/^nistp(\d+)$/, 'NIST P-$1');
65 var len2 = off < key.length ? this.lengthDecode(key, off) : 0;
75 var comment = parts.slice(2).join(' '),
76 fprint = parts[1].length > 68 ? parts[1].substr(0, 33) + '…' + parts[1].substr(-34) : parts[1];
81 return { type: 'RSA', bits: len2 * 8, comment: comment, fprint: fprint };
84 return { type: 'DSA', bits: len1 * 8, comment: comment, fprint: fprint };
87 return { type: 'ECDH', curve: 'Curve25519', comment: comment, fprint: fprint };
90 return { type: 'ECDSA', curve: curve, comment: comment, fprint: fprint };
98 function renderKeys(keys) {
99 var list = document.querySelector('.cbi-dynlist');
101 while (!matchesElem(list.firstElementChild, '.add-item'))
102 list.removeChild(list.firstElementChild);
104 keys.forEach(function(key) {
105 var pubkey = SSHPubkeyDecoder.decode(key);
107 list.insertBefore(E('div', {
112 E('strong', pubkey.comment || _('Unnamed key')), E('br'),
114 '%s, %s'.format(pubkey.type, pubkey.curve || _('%d Bit').format(pubkey.bits)),
115 E('br'), E('code', pubkey.fprint)
117 ]), list.lastElementChild);
120 if (list.firstElementChild === list.lastElementChild)
121 list.insertBefore(E('p', _('No public keys present yet.')), list.lastElementChild);
124 function saveKeys(keys) {
125 return fs.write('/etc/dropbear/authorized_keys', keys.join('\n') + '\n', 384 /* 0600 */)
126 .then(renderKeys.bind(this, keys))
127 .catch(function(e) { ui.addNotification(null, E('p', e.message)) })
128 .finally(ui.hideModal);
131 function addKey(ev) {
132 var list = findParent(ev.target, '.cbi-dynlist'),
133 input = list.querySelector('input[type="text"]'),
134 key = input.value.trim(),
135 pubkey = SSHPubkeyDecoder.decode(key),
141 list.querySelectorAll('.item').forEach(function(item) {
142 keys.push(item.getAttribute('data-key'));
145 if (keys.indexOf(key) !== -1) {
146 ui.showModal(_('Add key'), [
147 E('div', { class: 'alert-message warning' }, _('The given SSH public key has already been added.')),
148 E('div', { class: 'right' }, E('div', { class: 'btn', click: L.hideModal }, _('Close')))
152 ui.showModal(_('Add key'), [
153 E('div', { class: 'alert-message warning' }, _('The given SSH public key is invalid. Please supply proper public RSA or ECDSA keys.')),
154 E('div', { class: 'right' }, E('div', { class: 'btn', click: L.hideModal }, _('Close')))
161 return saveKeys(keys).then(function() {
162 var added = list.querySelector('[data-key="%s"]'.format(key));
164 added.classList.add('flash');
169 function removeKey(ev) {
170 var list = findParent(ev.target, '.cbi-dynlist'),
171 delkey = ev.target.getAttribute('data-key'),
174 list.querySelectorAll('.item').forEach(function(item) {
175 var key = item.getAttribute('data-key');
180 L.showModal(_('Delete key'), [
181 E('div', _('Do you really want to delete the following SSH key?')),
183 E('div', { class: 'right' }, [
184 E('div', { class: 'btn', click: L.hideModal }, _('Cancel')),
186 E('div', { class: 'btn danger', click: ui.createHandlerFn(this, saveKeys, keys) }, _('Delete key')),
191 function dragKey(ev) {
192 ev.stopPropagation();
194 ev.dataTransfer.dropEffect = 'copy';
197 function dropKey(ev) {
198 var file = ev.dataTransfer.files[0],
199 input = ev.currentTarget.querySelector('input[type="text"]'),
200 reader = new FileReader();
203 reader.onload = function(rev) {
204 input.value = rev.target.result.trim();
209 reader.readAsText(file);
212 ev.stopPropagation();
216 function handleWindowDragDropIgnore(ev) {
222 return fs.lines('/etc/dropbear/authorized_keys').then(function(lines) {
223 return lines.filter(function(line) {
224 return line.match(/^(ssh-rsa|ssh-dss|ssh-ed25519|ecdsa-sha2)\b/) != null;
229 render: function(keys) {
230 var list = E('div', {
231 'class': 'cbi-dynlist',
232 'dragover': isReadonlyView ? null : dragKey,
233 'drop': isReadonlyView ? null : dropKey
235 E('div', { 'class': 'add-item' }, [
237 'class': 'cbi-input-text',
239 'placeholder': _('Paste or drag SSH key file…') ,
240 'keydown': function(ev) { if (ev.keyCode === 13) addKey(ev) },
241 'disabled': isReadonlyView
244 'class': 'cbi-button',
245 'click': ui.createHandlerFn(this, addKey),
246 'disabled': isReadonlyView
251 keys.forEach(L.bind(function(key) {
252 var pubkey = SSHPubkeyDecoder.decode(key);
254 list.insertBefore(E('div', {
256 click: isReadonlyView ? null : ui.createHandlerFn(this, removeKey),
259 E('strong', pubkey.comment || _('Unnamed key')), E('br'),
261 '%s, %s'.format(pubkey.type, pubkey.curve || _('%d Bit').format(pubkey.bits)),
262 E('br'), E('code', pubkey.fprint)
264 ]), list.lastElementChild);
267 if (list.firstElementChild === list.lastElementChild)
268 list.insertBefore(E('p', _('No public keys present yet.')), list.lastElementChild);
270 window.addEventListener('dragover', handleWindowDragDropIgnore);
271 window.addEventListener('drop', handleWindowDragDropIgnore);
273 return E('div', {}, [
274 E('h2', _('SSH-Keys')),
275 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.')),
276 E('div', { 'class': 'cbi-section-node' }, list)
280 handleSaveApply: null,