4 var SSHPubkeyDecoder = L.Class.singleton({
5 lengthDecode: function(s, off)
7 var l = (s.charCodeAt(off++) << 24) |
8 (s.charCodeAt(off++) << 16) |
9 (s.charCodeAt(off++) << 8) |
12 if (l < 0 || (off + l) > s.length)
20 var parts = s.split(/\s+/);
25 try { key = atob(parts[1]); } catch(e) {}
32 len = this.lengthDecode(key, off);
37 var type = key.substr(off + 4, len);
38 if (type !== parts[0])
43 var len1 = off < key.length ? this.lengthDecode(key, off) : 0;
48 if (type.indexOf('ecdsa-sha2-') === 0) {
49 curve = key.substr(off + 4, len1);
51 if (!len1 || type.substr(11) !== curve)
55 curve = curve.replace(/^nistp(\d+)$/, 'NIST P-$1');
60 var len2 = off < key.length ? this.lengthDecode(key, off) : 0;
70 var comment = parts.slice(2).join(' '),
71 fprint = parts[1].length > 68 ? parts[1].substr(0, 33) + '…' + parts[1].substr(-34) : parts[1];
76 return { type: 'RSA', bits: len2 * 8, comment: comment, fprint: fprint };
79 return { type: 'DSA', bits: len1 * 8, comment: comment, fprint: fprint };
82 return { type: 'ECDH', curve: 'Curve25519', comment: comment, fprint: fprint };
85 return { type: 'ECDSA', curve: curve, comment: comment, fprint: fprint };
93 var callFileRead = rpc.declare({
100 var callFileWrite = rpc.declare({
103 params: [ 'path', 'data' ]
106 function renderKeys(keys) {
107 var list = document.querySelector('.cbi-dynlist[name="sshkeys"]');
109 while (!matchesElem(list.firstElementChild, '.add-item'))
110 list.removeChild(list.firstElementChild);
112 keys.forEach(function(key) {
113 var pubkey = SSHPubkeyDecoder.decode(key);
115 list.insertBefore(E('div', {
120 E('strong', pubkey.comment || _('Unnamed key')), E('br'),
122 '%s, %s'.format(pubkey.type, pubkey.curve || _('%d Bit').format(pubkey.bits)),
123 E('br'), E('code', pubkey.fprint)
125 ]), list.lastElementChild);
128 if (list.firstElementChild === list.lastElementChild)
129 list.insertBefore(E('p', _('No public keys present yet.')), list.lastElementChild);
132 function saveKeys(keys) {
133 return callFileWrite('/etc/dropbear/authorized_keys', keys.join('\n') + '\n')
134 .then(renderKeys.bind(this, keys))
135 .then(L.ui.hideModal);
138 function addKey(ev) {
139 var list = findParent(ev.target, '.cbi-dynlist'),
140 input = list.querySelector('input[type="text"]'),
141 key = input.value.trim(),
142 pubkey = SSHPubkeyDecoder.decode(key),
148 list.querySelectorAll('.item').forEach(function(item) {
149 keys.push(item.getAttribute('data-key'));
152 if (keys.indexOf(key) !== -1) {
153 L.ui.showModal(_('Add key'), [
154 E('div', { class: 'alert-message warning' }, _('The given SSH public key has already been added.')),
155 E('div', { class: 'right' }, E('div', { class: 'btn', click: L.hideModal }, _('Close')))
159 L.ui.showModal(_('Add key'), [
160 E('div', { class: 'alert-message warning' }, _('The given SSH public key is invalid. Please supply proper public RSA or ECDSA keys.')),
161 E('div', { class: 'right' }, E('div', { class: 'btn', click: L.hideModal }, _('Close')))
168 return saveKeys(keys).then(function() {
169 var added = list.querySelector('[data-key="%s"]'.format(key));
171 added.classList.add('flash');
176 function removeKey(ev) {
177 var list = findParent(ev.target, '.cbi-dynlist'),
178 delkey = ev.target.getAttribute('data-key'),
181 list.querySelectorAll('.item').forEach(function(item) {
182 var key = item.getAttribute('data-key');
187 L.showModal(_('Delete key'), [
188 E('div', _('Do you really want to delete the following SSH key?')),
190 E('div', { class: 'right' }, [
191 E('div', { class: 'btn', click: L.hideModal }, _('Cancel')),
193 E('div', { class: 'btn danger', click: L.ui.createHandlerFn(this, saveKeys, keys) }, _('Delete key')),
198 function dragKey(ev) {
199 ev.stopPropagation();
201 ev.dataTransfer.dropEffect = 'copy';
204 function dropKey(ev) {
205 var file = ev.dataTransfer.files[0],
206 input = ev.currentTarget.querySelector('input[type="text"]'),
207 reader = new FileReader();
210 reader.onload = function(rev) {
211 input.value = rev.target.result.trim();
216 reader.readAsText(file);
219 ev.stopPropagation();
223 function handleWindowDragDropIgnore(ev) {
227 return L.view.extend({
229 return callFileRead('/etc/dropbear/authorized_keys').then(function(data) {
230 return (data || '').split(/\n/).map(function(line) {
232 }).filter(function(line) {
233 return line.match(/^ssh-/) != null;
238 render: function(keys) {
239 var list = E('div', { 'class': 'cbi-dynlist', 'dragover': dragKey, 'drop': dropKey }, [
240 E('div', { 'class': 'add-item' }, [
242 'class': 'cbi-input-text',
244 'placeholder': _('Paste or drag SSH key file…') ,
245 'keydown': function(ev) { if (ev.keyCode === 13) addKey(ev) }
248 'class': 'cbi-button',
249 'click': L.ui.createHandlerFn(this, addKey)
254 keys.forEach(L.bind(function(key) {
255 var pubkey = SSHPubkeyDecoder.decode(key);
257 list.insertBefore(E('div', {
259 click: L.ui.createHandlerFn(this, removeKey),
262 E('strong', pubkey.comment || _('Unnamed key')), E('br'),
264 '%s, %s'.format(pubkey.type, pubkey.curve || _('%d Bit').format(pubkey.bits)),
265 E('br'), E('code', pubkey.fprint)
267 ]), list.lastElementChild);
270 if (list.firstElementChild === list.lastElementChild)
271 list.insertBefore(E('p', _('No public keys present yet.')), list.lastElementChild);
273 window.addEventListener('dragover', handleWindowDragDropIgnore);
274 window.addEventListener('drop', handleWindowDragDropIgnore);
276 return E('div', {}, [
277 E('h2', _('SSH-Keys')),
278 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.')),
279 E('div', { 'class': 'cbi-section-node' }, list)
283 handleSaveApply: null,