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
.trim().match(/^((?:(?:^|,)[^ =,]+(?:=(?:[^ ",]+|"(?:[^"\\]|\\.)*"))?)+ +)?(ssh-dss|ssh-rsa|ssh-ed25519|ecdsa-sha2-nistp[0-9]+|sk-ecdsa-sha2-nistp256@openssh\.com|sk-ssh-ed25519@openssh\.com) +([^ ]+)( +.*)?$/);
31 try { key
= atob(parts
[3]); } catch(e
) {}
38 len
= this.lengthDecode(key
, off
);
43 var type
= key
.substr(off
+ 4, len
);
44 if (type
!== parts
[2])
49 var len1
= off
< key
.length
? this.lengthDecode(key
, off
) : 0;
54 if (type
.indexOf('ecdsa-sha2-') === 0) {
55 curve
= key
.substr(off
+ 4, len1
);
57 if (!len1
|| type
.substr(11) !== curve
)
61 curve
= curve
.replace(/^nistp(\d+)$/, 'NIST P-$1');
66 var len2
= off
< key
.length
? this.lengthDecode(key
, off
) : 0;
76 var comment
= (parts
[4] || '').trim(),
77 fprint
= parts
[3].length
> 68 ? parts
[3].substr(0, 33) + '…' + parts
[3].substr(-34) : parts
[3];
80 (parts
[1] || '').trim().replace(/(?:^|,)([^ =,]+)(?:=(?:([^ ",]+)|"((?:[^"\\]|\\.)*)"))?/g, function(m
, k
, p
, q
) {
81 options
= options
|| {};
83 if (options
.hasOwnProperty(k
))
84 options
[k
] += ',' + (q
|| p
|| true);
86 options
[k
] = (q
|| p
|| true);
92 return { type
: 'RSA', bits
: len2
* 8, comment
: comment
, options
: options
, fprint
: fprint
, src
: s
};
95 return { type
: 'DSA', bits
: len1
* 8, comment
: comment
, options
: options
, fprint
: fprint
, src
: s
};
98 return { type
: 'EdDSA', curve
: 'Curve25519', comment
: comment
, options
: options
, fprint
: fprint
, src
: s
};
101 return { type
: 'ECDSA', curve
: curve
, comment
: comment
, options
: options
, fprint
: fprint
, src
: s
};
103 case 'sk-ecdsa-sha2-nistp256@openssh.com':
104 return { type
: 'ECDSA-SK', curve
: 'NIST P-256', comment
: comment
, options
: options
, fprint
: fprint
, src
: s
};
106 case 'sk-ssh-ed25519@openssh.com':
107 return { type
: 'EdDSA-SK', curve
: 'Curve25519', comment
: comment
, options
: options
, fprint
: fprint
, src
: s
};
115 function renderKeyItem(pubkey
) {
118 click
: isReadonlyView
? null : removeKey
,
119 'data-key': pubkey
.src
121 E('strong', [ pubkey
.comment
|| _('Unnamed key') ]), E('br'),
123 '%s, %s'.format(pubkey
.type
, pubkey
.curve
|| _('%d Bit').format(pubkey
.bits
)),
124 pubkey
.options
? E([], [
125 ' / ', _('Options:'), ' ',
126 E('code', Object
.keys(pubkey
.options
).sort().join(', '))
128 E('br'), E('code', pubkey
.fprint
)
133 function renderKeys(keys
) {
134 var list
= document
.querySelector('.cbi-dynlist');
136 while (!matchesElem(list
.firstElementChild
, '.add-item'))
137 list
.removeChild(list
.firstElementChild
);
139 keys
.forEach(function(key
) {
140 var pubkey
= SSHPubkeyDecoder
.decode(key
);
142 list
.insertBefore(renderKeyItem(pubkey
), list
.lastElementChild
);
145 if (list
.firstElementChild
=== list
.lastElementChild
)
146 list
.insertBefore(E('p', _('No public keys present yet.')), list
.lastElementChild
);
149 function saveKeys(keys
) {
150 return fs
.write('/etc/dropbear/authorized_keys', keys
.join('\n') + '\n', 384 /* 0600 */)
151 .then(renderKeys
.bind(this, keys
))
152 .catch(function(e
) { ui
.addNotification(null, E('p', e
.message
)) })
153 .finally(ui
.hideModal
);
156 function addKey(ev
) {
157 var list
= findParent(ev
.target
, '.cbi-dynlist'),
158 input
= list
.querySelector('input[type="text"]'),
159 key
= input
.value
.trim(),
160 pubkey
= SSHPubkeyDecoder
.decode(key
),
166 list
.querySelectorAll('.item').forEach(function(item
) {
167 keys
.push(item
.getAttribute('data-key'));
170 if (keys
.indexOf(key
) !== -1) {
171 ui
.showModal(_('Add key'), [
172 E('div', { class: 'alert-message warning' }, _('The given SSH public key has already been added.')),
173 E('div', { class: 'right' }, E('div', { class: 'btn', click
: L
.hideModal
}, _('Close')))
177 ui
.showModal(_('Add key'), [
178 E('div', { class: 'alert-message warning' }, _('The given SSH public key is invalid. Please supply proper public RSA, ED25519 or ECDSA keys.')),
179 E('div', { class: 'right' }, E('div', { class: 'btn', click
: L
.hideModal
}, _('Close')))
186 return saveKeys(keys
).then(function() {
187 var added
= list
.querySelector('[data-key="%s"]'.format(key
.replace(/["\\]/g, '\\$&')));
189 added
.classList
.add('flash');
194 function removeKey(ev
) {
195 var list
= findParent(ev
.target
, '.cbi-dynlist'),
196 delkey
= ev
.target
.getAttribute('data-key'),
199 list
.querySelectorAll('.item').forEach(function(item
) {
200 var key
= item
.getAttribute('data-key');
205 L
.showModal(_('Delete key'), [
206 E('div', _('Do you really want to delete the following SSH key?')),
208 E('div', { class: 'right' }, [
209 E('div', { class: 'btn', click
: L
.hideModal
}, _('Cancel')),
211 E('div', { class: 'btn danger', click
: ui
.createHandlerFn(this, saveKeys
, keys
) }, _('Delete key')),
216 function dragKey(ev
) {
217 ev
.stopPropagation();
219 ev
.dataTransfer
.dropEffect
= 'copy';
222 function dropKey(ev
) {
223 var file
= ev
.dataTransfer
.files
[0],
224 input
= ev
.currentTarget
.querySelector('input[type="text"]'),
225 reader
= new FileReader();
228 reader
.onload = function(rev
) {
229 input
.value
= rev
.target
.result
.trim();
234 reader
.readAsText(file
);
237 ev
.stopPropagation();
241 function handleWindowDragDropIgnore(ev
) {
247 return fs
.lines('/etc/dropbear/authorized_keys').then(function(lines
) {
248 return lines
.map(function(line
) {
249 return SSHPubkeyDecoder
.decode(line
);
250 }).filter(function(line
) {
256 render: function(keys
) {
257 var list
= E('div', {
258 'class': 'cbi-dynlist',
259 'dragover': isReadonlyView
? null : dragKey
,
260 'drop': isReadonlyView
? null : dropKey
262 E('div', { 'class': 'add-item' }, [
264 'class': 'cbi-input-text',
266 'placeholder': _('Paste or drag SSH key file…') ,
267 'keydown': function(ev
) { if (ev
.keyCode
=== 13) addKey(ev
) },
268 'disabled': isReadonlyView
271 'class': 'cbi-button',
272 'click': ui
.createHandlerFn(this, addKey
),
273 'disabled': isReadonlyView
278 keys
.forEach(L
.bind(function(pubkey
) {
279 list
.insertBefore(renderKeyItem(pubkey
), list
.lastElementChild
);
282 if (list
.firstElementChild
=== list
.lastElementChild
)
283 list
.insertBefore(E('p', _('No public keys present yet.')), list
.lastElementChild
);
285 window
.addEventListener('dragover', handleWindowDragDropIgnore
);
286 window
.addEventListener('drop', handleWindowDragDropIgnore
);
288 return E('div', {}, [
289 E('h2', _('SSH-Keys')),
290 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.')),
291 E('div', { 'class': 'cbi-section-node' }, list
)
295 handleSaveApply
: null,