]>
Commit | Line | Data |
---|---|---|
d1a7c6ee | 1 | /*global QRCode*/ |
d1a7c6ee TL |
2 | Ext.define('PVE.Storage.PBSKeyShow', { |
3 | extend: 'Ext.window.Window', | |
cfc3a166 | 4 | xtype: 'pvePBSKeyShow', |
d1a7c6ee TL |
5 | mixins: ['Proxmox.Mixin.CBind'], |
6 | ||
7 | width: 600, | |
8 | modal: true, | |
9 | resizable: false, | |
10 | title: gettext('Important: Save your Encryption Key'), | |
11 | ||
12 | // avoid that esc closes this by mistake, force user to more manual action | |
13 | onEsc: Ext.emptyFn, | |
14 | closable: false, | |
15 | ||
16 | items: [ | |
17 | { | |
18 | xtype: 'form', | |
19 | layout: { | |
20 | type: 'vbox', | |
21 | align: 'stretch', | |
22 | }, | |
23 | bodyPadding: 10, | |
24 | border: false, | |
25 | defaults: { | |
26 | anchor: '100%', | |
27 | border: false, | |
28 | padding: '10 0 0 0', | |
29 | }, | |
30 | items: [ | |
31 | { | |
32 | xtype: 'textfield', | |
33 | fieldLabel: gettext('Key'), | |
13a04340 | 34 | labelWidth: 80, |
d1a7c6ee TL |
35 | inputId: 'encryption-key-value', |
36 | cbind: { | |
37 | value: '{key}', | |
38 | }, | |
39 | editable: false, | |
40 | }, | |
41 | { | |
42 | xtype: 'component', | |
a6f6ed5a | 43 | html: gettext('Keep your encryption key safe, but easily accessible for disaster recovery.') |
d1a7c6ee TL |
44 | + '<br>' + gettext('We recommend the following safe-keeping strategy:'), |
45 | }, | |
46 | { | |
47 | xtyp: 'container', | |
48 | layout: 'hbox', | |
49 | items: [ | |
50 | { | |
51 | xtype: 'component', | |
52 | html: '1. ' + gettext('Save the key in your password manager.'), | |
53 | flex: 1, | |
54 | }, | |
55 | { | |
56 | xtype: 'button', | |
57 | text: gettext('Copy Key'), | |
58 | iconCls: 'fa fa-clipboard x-btn-icon-el-default-toolbar-small', | |
59 | cls: 'x-btn-default-toolbar-small proxmox-inline-button', | |
60 | width: 110, | |
61 | handler: function(b) { | |
62 | document.getElementById('encryption-key-value').select(); | |
63 | document.execCommand("copy"); | |
64 | }, | |
65 | }, | |
66 | ], | |
67 | }, | |
68 | { | |
69 | xtype: 'container', | |
70 | layout: 'hbox', | |
71 | items: [ | |
72 | { | |
73 | xtype: 'component', | |
74 | html: '2. ' + gettext('Download the key to a USB (pen) drive, placed in secure vault.'), | |
75 | flex: 1, | |
76 | }, | |
77 | { | |
78 | xtype: 'button', | |
79 | text: gettext('Download'), | |
80 | iconCls: 'fa fa-download x-btn-icon-el-default-toolbar-small', | |
81 | cls: 'x-btn-default-toolbar-small proxmox-inline-button', | |
82 | width: 110, | |
83 | handler: function(b) { | |
84 | let win = this.up('window'); | |
85 | ||
86 | let pveID = PVE.ClusterName || window.location.hostname; | |
87 | let name = `pve-${pveID}-storage-${win.sid}.enc`; | |
88 | ||
89 | let hiddenElement = document.createElement('a'); | |
90 | hiddenElement.href = 'data:attachment/text,' + encodeURI(win.key); | |
91 | hiddenElement.target = '_blank'; | |
92 | hiddenElement.download = name; | |
93 | hiddenElement.click(); | |
94 | }, | |
95 | }, | |
96 | ], | |
97 | }, | |
98 | { | |
99 | xtype: 'container', | |
100 | layout: 'hbox', | |
101 | items: [ | |
102 | { | |
103 | xtype: 'component', | |
104 | html: '3. ' + gettext('Print as paperkey, laminated and placed in secure vault.'), | |
105 | flex: 1, | |
106 | }, | |
107 | { | |
108 | xtype: 'button', | |
109 | text: gettext('Print Key'), | |
110 | iconCls: 'fa fa-print x-btn-icon-el-default-toolbar-small', | |
111 | cls: 'x-btn-default-toolbar-small proxmox-inline-button', | |
112 | width: 110, | |
113 | handler: function(b) { | |
114 | let win = this.up('window'); | |
115 | win.paperkey(win.key); | |
116 | }, | |
117 | }, | |
118 | ], | |
119 | }, | |
120 | ], | |
121 | }, | |
122 | { | |
123 | xtype: 'component', | |
124 | border: false, | |
125 | padding: '10 10 10 10', | |
126 | userCls: 'pmx-hint', | |
ba854589 | 127 | html: gettext('Please save the encryption key - losing it will render any backup created with it unusable'), |
d1a7c6ee TL |
128 | }, |
129 | ], | |
130 | buttons: [ | |
131 | { | |
132 | text: gettext('Close'), | |
133 | handler: function(b) { | |
134 | let win = this.up('window'); | |
135 | win.close(); | |
136 | }, | |
137 | }, | |
138 | ], | |
cfc3a166 | 139 | paperkey: function(keyString) { |
d1a7c6ee TL |
140 | let me = this; |
141 | ||
cfc3a166 TL |
142 | const key = JSON.parse(keyString); |
143 | ||
d1a7c6ee TL |
144 | const qrwidth = 500; |
145 | let qrdiv = document.createElement('div'); | |
146 | let qrcode = new QRCode(qrdiv, { | |
147 | width: qrwidth, | |
148 | height: qrwidth, | |
149 | correctLevel: QRCode.CorrectLevel.H, | |
150 | }); | |
cfc3a166 TL |
151 | qrcode.makeCode(keyString); |
152 | ||
153 | let shortKeyFP = ''; | |
154 | if (key.fingerprint) { | |
155 | shortKeyFP = PVE.Utils.render_pbs_fingerprint(key.fingerprint); | |
156 | } | |
d1a7c6ee TL |
157 | |
158 | let printFrame = document.createElement("iframe"); | |
159 | Object.assign(printFrame.style, { | |
160 | position: "fixed", | |
161 | right: "0", | |
162 | bottom: "0", | |
163 | width: "0", | |
164 | height: "0", | |
165 | border: "0", | |
166 | }); | |
cfc3a166 | 167 | const prettifiedKey = JSON.stringify(key, null, 2); |
d1a7c6ee TL |
168 | const keyQrBase64 = qrdiv.children[0].toDataURL("image/png"); |
169 | const html = `<html><head><script> | |
170 | window.addEventListener('DOMContentLoaded', (ev) => window.print()); | |
171 | </script><style>@media print and (max-height: 150mm) { | |
172 | h4, p { margin: 0; font-size: 1em; } | |
173 | }</style></head><body style="padding: 5px;"> | |
cfc3a166 TL |
174 | <h4>Encryption Key - Storage '${me.sid}' (${shortKeyFP})</h4> |
175 | <p style="font-size:1.2em;font-family:monospace;white-space:pre-wrap;overflow-wrap:break-word;"> | |
d1a7c6ee TL |
176 | -----BEGIN PROXMOX BACKUP KEY----- |
177 | ${prettifiedKey} | |
178 | -----END PROXMOX BACKUP KEY-----</p> | |
179 | <center><img style="width: 100%; max-width: ${qrwidth}px;" src="${keyQrBase64}"></center> | |
180 | </body></html>`; | |
181 | ||
182 | printFrame.src = "data:text/html;base64," + btoa(html); | |
183 | document.body.appendChild(printFrame); | |
184 | }, | |
185 | }); | |
186 | ||
14ba33fb TL |
187 | Ext.define('PVE.panel.PBSEncryptionKeyTab', { |
188 | extend: 'Proxmox.panel.InputPanel', | |
189 | xtype: 'pvePBSEncryptionKeyTab', | |
190 | mixins: ['Proxmox.Mixin.CBind'], | |
191 | ||
903e646d TL |
192 | onlineHelp: 'storage_pbs_encryption', |
193 | ||
14ba33fb TL |
194 | onGetValues: function(form) { |
195 | let values = {}; | |
196 | if (form.cryptMode === 'upload') { | |
197 | values['encryption-key'] = form['crypt-key-upload']; | |
198 | } else if (form.cryptMode === 'autogenerate') { | |
199 | values['encryption-key'] = 'autogen'; | |
200 | } else if (form.cryptMode === 'none') { | |
201 | if (!this.isCreate) { | |
202 | values.delete = ['encryption-key']; | |
203 | } | |
204 | } | |
205 | return values; | |
206 | }, | |
207 | ||
208 | setValues: function(values) { | |
209 | let me = this; | |
210 | let vm = me.getViewModel(); | |
211 | ||
212 | let cryptKeyInfo = values['encryption-key']; | |
213 | if (cryptKeyInfo) { | |
8058410f | 214 | let icon = '<span class="fa fa-lock good"></span> '; |
14ba33fb TL |
215 | if (cryptKeyInfo.match(/^[a-fA-F0-9]{2}:/)) { // new style fingerprint |
216 | let shortKeyFP = PVE.Utils.render_pbs_fingerprint(cryptKeyInfo); | |
217 | values['crypt-key-fp'] = icon + `${gettext('Active')} - ${gettext('Fingerprint')} ${shortKeyFP}`; | |
218 | } else { | |
219 | // old key without FP | |
220 | values['crypt-key-fp'] = icon + gettext('Active'); | |
221 | } | |
222 | } else { | |
223 | values['crypt-key-fp'] = gettext('None'); | |
224 | let cryptModeNone = me.down('radiofield[inputValue=none]'); | |
225 | cryptModeNone.setBoxLabel(gettext('Do not encrypt backups')); | |
226 | cryptModeNone.setValue(true); | |
227 | } | |
228 | vm.set('keepCryptVisible', !!cryptKeyInfo); | |
229 | vm.set('allowEdit', !cryptKeyInfo); | |
230 | ||
231 | me.callParent([values]); | |
232 | }, | |
233 | ||
234 | viewModel: { | |
235 | data: { | |
236 | allowEdit: true, | |
237 | keepCryptVisible: false, | |
238 | }, | |
239 | formulas: { | |
240 | showDangerousHint: get => { | |
241 | let allowEdit = get('allowEdit'); | |
242 | return get('keepCryptVisible') && allowEdit; | |
243 | }, | |
244 | }, | |
245 | }, | |
246 | ||
247 | items: [ | |
248 | { | |
249 | xtype: 'displayfield', | |
250 | name: 'crypt-key-fp', | |
251 | fieldLabel: gettext('Encryption Key'), | |
252 | padding: '2 0', | |
253 | }, | |
254 | { | |
255 | xtype: 'checkbox', | |
256 | name: 'crypt-allow-edit', | |
257 | boxLabel: gettext('Edit existing encryption key (dangerous!)'), | |
258 | hidden: true, | |
259 | submitValue: false, | |
260 | isDirty: () => false, | |
261 | bind: { | |
262 | hidden: '{!keepCryptVisible}', | |
263 | value: '{allowEdit}', | |
264 | }, | |
265 | }, | |
266 | { | |
267 | xtype: 'radiofield', | |
268 | name: 'cryptMode', | |
269 | inputValue: 'keep', | |
270 | boxLabel: gettext('Keep encryption key'), | |
271 | padding: '0 0 0 25', | |
272 | cbind: { | |
273 | hidden: '{isCreate}', | |
274 | checked: '{!isCreate}', | |
275 | }, | |
276 | bind: { | |
277 | hidden: '{!keepCryptVisible}', | |
278 | disabled: '{!allowEdit}', | |
279 | }, | |
280 | }, | |
281 | { | |
282 | xtype: 'radiofield', | |
283 | name: 'cryptMode', | |
284 | inputValue: 'none', | |
285 | checked: true, | |
286 | padding: '0 0 0 25', | |
287 | cbind: { | |
288 | disabled: '{!isCreate}', | |
289 | checked: '{isCreate}', | |
290 | boxLabel: get => get('isCreate') | |
291 | ? gettext('Do not encrypt backups') | |
292 | : gettext('Delete existing encryption key'), | |
293 | }, | |
294 | bind: { | |
295 | disabled: '{!allowEdit}', | |
296 | }, | |
297 | }, | |
298 | { | |
299 | xtype: 'radiofield', | |
300 | name: 'cryptMode', | |
301 | inputValue: 'autogenerate', | |
302 | boxLabel: gettext('Auto-generate a client encryption key'), | |
303 | padding: '0 0 0 25', | |
304 | cbind: { | |
305 | disabled: '{!isCreate}', | |
306 | }, | |
307 | bind: { | |
308 | disabled: '{!allowEdit}', | |
309 | }, | |
310 | }, | |
311 | { | |
312 | xtype: 'radiofield', | |
313 | name: 'cryptMode', | |
314 | inputValue: 'upload', | |
315 | boxLabel: gettext('Upload an existing client encryption key'), | |
316 | padding: '0 0 0 25', | |
317 | cbind: { | |
318 | disabled: '{!isCreate}', | |
319 | }, | |
320 | bind: { | |
321 | disabled: '{!allowEdit}', | |
322 | }, | |
323 | listeners: { | |
324 | change: function(f, value) { | |
325 | let panel = this.up('inputpanel'); | |
326 | if (!panel.rendered) { | |
327 | return; | |
328 | } | |
329 | let uploadKeyField = panel.down('field[name=crypt-key-upload]'); | |
330 | uploadKeyField.setDisabled(!value); | |
331 | uploadKeyField.setHidden(!value); | |
332 | ||
333 | let uploadKeyButton = panel.down('filebutton[name=crypt-upload-button]'); | |
334 | uploadKeyButton.setDisabled(!value); | |
335 | uploadKeyButton.setHidden(!value); | |
336 | ||
337 | if (value) { | |
338 | uploadKeyField.validate(); | |
339 | } else { | |
340 | uploadKeyField.reset(); | |
341 | } | |
342 | }, | |
343 | }, | |
344 | }, | |
345 | { | |
346 | xtype: 'fieldcontainer', | |
347 | layout: 'hbox', | |
348 | items: [ | |
349 | { | |
350 | xtype: 'proxmoxtextfield', | |
351 | name: 'crypt-key-upload', | |
352 | fieldLabel: gettext('Key'), | |
353 | value: '', | |
354 | disabled: true, | |
355 | hidden: true, | |
356 | allowBlank: false, | |
357 | labelAlign: 'right', | |
358 | flex: 1, | |
359 | emptyText: gettext('You can drag-and-drop a key file here.'), | |
360 | validator: function(value) { | |
361 | if (value.length) { | |
362 | let key; | |
363 | try { | |
364 | key = JSON.parse(value); | |
365 | } catch (e) { | |
366 | return "Failed to parse key - " + e; | |
367 | } | |
368 | if (typeof key.data === undefined) { | |
369 | return "Does not seems like a valid Proxmox Backup key!"; | |
370 | } | |
371 | } | |
372 | return true; | |
373 | }, | |
374 | afterRender: function() { | |
14ba33fb TL |
375 | if (!window.FileReader) { |
376 | // No FileReader support in this browser | |
377 | return; | |
378 | } | |
379 | let cancel = function(ev) { | |
380 | ev = ev.event; | |
381 | if (ev.preventDefault) { | |
382 | ev.preventDefault(); | |
383 | } | |
384 | }; | |
f9fa8322 DJ |
385 | this.inputEl.on('dragover', cancel); |
386 | this.inputEl.on('dragenter', cancel); | |
387 | this.inputEl.on('drop', ev => { | |
388 | cancel(ev); | |
389 | let files = ev.event.dataTransfer.files; | |
390 | PVE.Utils.loadTextFromFile(files[0], v => this.setValue(v)); | |
14ba33fb TL |
391 | }); |
392 | }, | |
393 | }, | |
394 | { | |
395 | xtype: 'filebutton', | |
396 | name: 'crypt-upload-button', | |
397 | iconCls: 'fa fa-fw fa-folder-open-o x-btn-icon-el-default-toolbar-small', | |
398 | cls: 'x-btn-default-toolbar-small proxmox-inline-button', | |
399 | margin: '0 0 0 4', | |
400 | disabled: true, | |
401 | hidden: true, | |
402 | listeners: { | |
403 | change: function(btn, e, value) { | |
404 | let ev = e.event; | |
405 | let field = btn.up().down('proxmoxtextfield[name=crypt-key-upload]'); | |
406 | PVE.Utils.loadTextFromFile(ev.target.files[0], v => field.setValue(v)); | |
407 | btn.reset(); | |
408 | }, | |
409 | }, | |
410 | }, | |
411 | ], | |
412 | }, | |
413 | { | |
414 | xtype: 'component', | |
415 | border: false, | |
416 | padding: '5 2', | |
417 | userCls: 'pmx-hint', | |
418 | html: // `<b style="color:red;font-weight:600;">${gettext('Warning')}</b>: ` + | |
419 | `<span class="fa fa-exclamation-triangle" style="color:red;font-size:14px;"></span> ` + | |
420 | gettext('Deleting or replacing the encryption key will break restoring backups created with it!'), | |
421 | hidden: true, | |
422 | bind: { | |
423 | hidden: '{!showDangerousHint}', | |
424 | }, | |
425 | }, | |
426 | ], | |
427 | }); | |
428 | ||
ee19d331 TL |
429 | Ext.define('PVE.storage.PBSInputPanel', { |
430 | extend: 'PVE.panel.StorageBase', | |
431 | ||
903e646d | 432 | onlineHelp: 'storage_pbs', |
ee19d331 | 433 | |
d1a7c6ee TL |
434 | apiCallDone: function(success, response, options) { |
435 | let res = response.result.data; | |
436 | if (!(res && res.config && res.config['encryption-key'])) { | |
437 | return; | |
438 | } | |
439 | let key = res.config['encryption-key']; | |
440 | Ext.create('PVE.Storage.PBSKeyShow', { | |
441 | autoShow: true, | |
442 | sid: res.storage, | |
443 | key: key, | |
444 | }); | |
445 | }, | |
446 | ||
5b7ab402 TL |
447 | isPBS: true, // HACK |
448 | ||
14ba33fb TL |
449 | extraTabs: [ |
450 | { | |
451 | xtype: 'pvePBSEncryptionKeyTab', | |
452 | title: gettext('Encryption'), | |
453 | }, | |
454 | ], | |
455 | ||
ae595bc1 LS |
456 | setValues: function(values) { |
457 | let me = this; | |
458 | ||
459 | let server = values.server; | |
460 | if (values.port !== undefined) { | |
461 | if (Proxmox.Utils.IP6_match.test(server)) { | |
462 | server = `[${server}]`; | |
463 | } | |
464 | server += `:${values.port}`; | |
465 | } | |
466 | values.hostport = server; | |
467 | ||
468 | return me.callParent([values]); | |
469 | }, | |
470 | ||
ee19d331 TL |
471 | initComponent: function() { |
472 | var me = this; | |
473 | ||
474 | me.column1 = [ | |
475 | { | |
ae595bc1 | 476 | xtype: me.isCreate ? 'proxmoxtextfield' : 'displayfield', |
ee19d331 TL |
477 | fieldLabel: gettext('Server'), |
478 | allowBlank: false, | |
ae595bc1 LS |
479 | name: 'hostport', |
480 | submitValue: false, | |
481 | vtype: 'HostPort', | |
482 | listeners: { | |
483 | change: function(field, newvalue) { | |
484 | let server = newvalue; | |
485 | let port; | |
486 | ||
487 | let match = Proxmox.Utils.HostPort_match.exec(newvalue); | |
488 | if (match === null) { | |
489 | match = Proxmox.Utils.HostPortBrackets_match.exec(newvalue); | |
490 | if (match === null) { | |
491 | match = Proxmox.Utils.IP6_dotnotation_match.exec(newvalue); | |
492 | } | |
493 | } | |
494 | ||
495 | if (match !== null) { | |
496 | server = match[1]; | |
497 | if (match[2] !== undefined) { | |
498 | port = match[2]; | |
499 | } | |
500 | } | |
501 | ||
502 | field.up('inputpanel').down('field[name=server]').setValue(server); | |
503 | field.up('inputpanel').down('field[name=port]').setValue(port); | |
504 | }, | |
505 | }, | |
506 | }, | |
507 | { | |
508 | xtype: 'proxmoxtextfield', | |
509 | hidden: true, | |
510 | name: 'server', | |
4b105ec5 | 511 | submitValue: me.isCreate, // it is fixed |
ae595bc1 LS |
512 | }, |
513 | { | |
514 | xtype: 'proxmoxtextfield', | |
515 | hidden: true, | |
516 | deleteEmpty: !me.isCreate, | |
517 | name: 'port', | |
ee19d331 TL |
518 | }, |
519 | { | |
520 | xtype: me.isCreate ? 'textfield' : 'displayfield', | |
521 | name: 'username', | |
522 | value: '', | |
523 | emptyText: gettext('Example') + ': admin@pbs', | |
524 | fieldLabel: gettext('Username'), | |
525 | regex: /\S+@\w+/, | |
526 | regexText: gettext('Example') + ': admin@pbs', | |
527 | allowBlank: false, | |
528 | }, | |
529 | { | |
530 | xtype: me.isCreate ? 'textfield' : 'displayfield', | |
531 | inputType: 'password', | |
532 | name: 'password', | |
533 | value: me.isCreate ? '' : '********', | |
534 | emptyText: me.isCreate ? gettext('None') : '', | |
535 | fieldLabel: gettext('Password'), | |
303dc33f | 536 | allowBlank: false, |
ee19d331 | 537 | }, |
ee19d331 TL |
538 | ]; |
539 | ||
540 | me.column2 = [ | |
ee19d331 TL |
541 | { |
542 | xtype: 'displayfield', | |
543 | name: 'content', | |
544 | value: 'backup', | |
545 | submitValue: true, | |
546 | fieldLabel: gettext('Content'), | |
547 | }, | |
db364c61 TL |
548 | { |
549 | xtype: me.isCreate ? 'textfield' : 'displayfield', | |
550 | name: 'datastore', | |
551 | value: '', | |
552 | fieldLabel: 'Datastore', | |
553 | allowBlank: false, | |
554 | }, | |
ee19d331 TL |
555 | ]; |
556 | ||
557 | me.columnB = [ | |
558 | { | |
76fa1b43 | 559 | xtype: 'proxmoxtextfield', |
ee19d331 | 560 | name: 'fingerprint', |
76fa1b43 | 561 | value: me.isCreate ? null : undefined, |
ee19d331 | 562 | fieldLabel: gettext('Fingerprint'), |
d3d1e199 | 563 | emptyText: gettext('Server certificate SHA-256 fingerprint, required for self-signed certificates'), |
ee19d331 TL |
564 | regex: /[A-Fa-f0-9]{2}(:[A-Fa-f0-9]{2}){31}/, |
565 | regexText: gettext('Example') + ': AB:CD:EF:...', | |
6c23fbbe | 566 | deleteEmpty: !me.isCreate, |
ee19d331 TL |
567 | allowBlank: true, |
568 | }, | |
569 | ]; | |
570 | ||
571 | me.callParent(); | |
572 | }, | |
573 | }); |