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,