]>
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); | |
faaae2a2 | 184 | me.on('destroy', () => document.body.removeChild(printFrame)); |
d1a7c6ee TL |
185 | }, |
186 | }); | |
187 | ||
14ba33fb TL |
188 | Ext.define('PVE.panel.PBSEncryptionKeyTab', { |
189 | extend: 'Proxmox.panel.InputPanel', | |
190 | xtype: 'pvePBSEncryptionKeyTab', | |
191 | mixins: ['Proxmox.Mixin.CBind'], | |
192 | ||
903e646d TL |
193 | onlineHelp: 'storage_pbs_encryption', |
194 | ||
14ba33fb TL |
195 | onGetValues: function(form) { |
196 | let values = {}; | |
197 | if (form.cryptMode === 'upload') { | |
198 | values['encryption-key'] = form['crypt-key-upload']; | |
199 | } else if (form.cryptMode === 'autogenerate') { | |
200 | values['encryption-key'] = 'autogen'; | |
201 | } else if (form.cryptMode === 'none') { | |
202 | if (!this.isCreate) { | |
203 | values.delete = ['encryption-key']; | |
204 | } | |
205 | } | |
206 | return values; | |
207 | }, | |
208 | ||
209 | setValues: function(values) { | |
210 | let me = this; | |
211 | let vm = me.getViewModel(); | |
212 | ||
213 | let cryptKeyInfo = values['encryption-key']; | |
214 | if (cryptKeyInfo) { | |
8058410f | 215 | let icon = '<span class="fa fa-lock good"></span> '; |
14ba33fb TL |
216 | if (cryptKeyInfo.match(/^[a-fA-F0-9]{2}:/)) { // new style fingerprint |
217 | let shortKeyFP = PVE.Utils.render_pbs_fingerprint(cryptKeyInfo); | |
218 | values['crypt-key-fp'] = icon + `${gettext('Active')} - ${gettext('Fingerprint')} ${shortKeyFP}`; | |
219 | } else { | |
220 | // old key without FP | |
221 | values['crypt-key-fp'] = icon + gettext('Active'); | |
222 | } | |
223 | } else { | |
224 | values['crypt-key-fp'] = gettext('None'); | |
225 | let cryptModeNone = me.down('radiofield[inputValue=none]'); | |
226 | cryptModeNone.setBoxLabel(gettext('Do not encrypt backups')); | |
227 | cryptModeNone.setValue(true); | |
228 | } | |
229 | vm.set('keepCryptVisible', !!cryptKeyInfo); | |
230 | vm.set('allowEdit', !cryptKeyInfo); | |
231 | ||
232 | me.callParent([values]); | |
233 | }, | |
234 | ||
235 | viewModel: { | |
236 | data: { | |
237 | allowEdit: true, | |
238 | keepCryptVisible: false, | |
239 | }, | |
240 | formulas: { | |
241 | showDangerousHint: get => { | |
242 | let allowEdit = get('allowEdit'); | |
243 | return get('keepCryptVisible') && allowEdit; | |
244 | }, | |
245 | }, | |
246 | }, | |
247 | ||
248 | items: [ | |
249 | { | |
250 | xtype: 'displayfield', | |
251 | name: 'crypt-key-fp', | |
252 | fieldLabel: gettext('Encryption Key'), | |
253 | padding: '2 0', | |
254 | }, | |
255 | { | |
256 | xtype: 'checkbox', | |
257 | name: 'crypt-allow-edit', | |
258 | boxLabel: gettext('Edit existing encryption key (dangerous!)'), | |
259 | hidden: true, | |
260 | submitValue: false, | |
261 | isDirty: () => false, | |
262 | bind: { | |
263 | hidden: '{!keepCryptVisible}', | |
264 | value: '{allowEdit}', | |
265 | }, | |
266 | }, | |
267 | { | |
268 | xtype: 'radiofield', | |
269 | name: 'cryptMode', | |
270 | inputValue: 'keep', | |
271 | boxLabel: gettext('Keep encryption key'), | |
272 | padding: '0 0 0 25', | |
273 | cbind: { | |
274 | hidden: '{isCreate}', | |
275 | checked: '{!isCreate}', | |
276 | }, | |
277 | bind: { | |
278 | hidden: '{!keepCryptVisible}', | |
279 | disabled: '{!allowEdit}', | |
280 | }, | |
281 | }, | |
282 | { | |
283 | xtype: 'radiofield', | |
284 | name: 'cryptMode', | |
285 | inputValue: 'none', | |
286 | checked: true, | |
287 | padding: '0 0 0 25', | |
288 | cbind: { | |
289 | disabled: '{!isCreate}', | |
290 | checked: '{isCreate}', | |
291 | boxLabel: get => get('isCreate') | |
292 | ? gettext('Do not encrypt backups') | |
293 | : gettext('Delete existing encryption key'), | |
294 | }, | |
295 | bind: { | |
296 | disabled: '{!allowEdit}', | |
297 | }, | |
298 | }, | |
299 | { | |
300 | xtype: 'radiofield', | |
301 | name: 'cryptMode', | |
302 | inputValue: 'autogenerate', | |
303 | boxLabel: gettext('Auto-generate a client encryption key'), | |
304 | padding: '0 0 0 25', | |
305 | cbind: { | |
306 | disabled: '{!isCreate}', | |
307 | }, | |
308 | bind: { | |
309 | disabled: '{!allowEdit}', | |
310 | }, | |
311 | }, | |
312 | { | |
313 | xtype: 'radiofield', | |
314 | name: 'cryptMode', | |
315 | inputValue: 'upload', | |
316 | boxLabel: gettext('Upload an existing client encryption key'), | |
317 | padding: '0 0 0 25', | |
318 | cbind: { | |
319 | disabled: '{!isCreate}', | |
320 | }, | |
321 | bind: { | |
322 | disabled: '{!allowEdit}', | |
323 | }, | |
324 | listeners: { | |
325 | change: function(f, value) { | |
326 | let panel = this.up('inputpanel'); | |
327 | if (!panel.rendered) { | |
328 | return; | |
329 | } | |
330 | let uploadKeyField = panel.down('field[name=crypt-key-upload]'); | |
331 | uploadKeyField.setDisabled(!value); | |
332 | uploadKeyField.setHidden(!value); | |
333 | ||
334 | let uploadKeyButton = panel.down('filebutton[name=crypt-upload-button]'); | |
335 | uploadKeyButton.setDisabled(!value); | |
336 | uploadKeyButton.setHidden(!value); | |
337 | ||
338 | if (value) { | |
339 | uploadKeyField.validate(); | |
340 | } else { | |
341 | uploadKeyField.reset(); | |
342 | } | |
343 | }, | |
344 | }, | |
345 | }, | |
346 | { | |
347 | xtype: 'fieldcontainer', | |
348 | layout: 'hbox', | |
349 | items: [ | |
350 | { | |
351 | xtype: 'proxmoxtextfield', | |
352 | name: 'crypt-key-upload', | |
353 | fieldLabel: gettext('Key'), | |
354 | value: '', | |
355 | disabled: true, | |
356 | hidden: true, | |
357 | allowBlank: false, | |
358 | labelAlign: 'right', | |
359 | flex: 1, | |
360 | emptyText: gettext('You can drag-and-drop a key file here.'), | |
361 | validator: function(value) { | |
362 | if (value.length) { | |
363 | let key; | |
364 | try { | |
365 | key = JSON.parse(value); | |
366 | } catch (e) { | |
367 | return "Failed to parse key - " + e; | |
368 | } | |
13799d5b | 369 | if (key.data === undefined) { |
14ba33fb TL |
370 | return "Does not seems like a valid Proxmox Backup key!"; |
371 | } | |
372 | } | |
373 | return true; | |
374 | }, | |
375 | afterRender: function() { | |
14ba33fb TL |
376 | if (!window.FileReader) { |
377 | // No FileReader support in this browser | |
378 | return; | |
379 | } | |
380 | let cancel = function(ev) { | |
381 | ev = ev.event; | |
382 | if (ev.preventDefault) { | |
383 | ev.preventDefault(); | |
384 | } | |
385 | }; | |
f9fa8322 DJ |
386 | this.inputEl.on('dragover', cancel); |
387 | this.inputEl.on('dragenter', cancel); | |
388 | this.inputEl.on('drop', ev => { | |
389 | cancel(ev); | |
390 | let files = ev.event.dataTransfer.files; | |
391 | PVE.Utils.loadTextFromFile(files[0], v => this.setValue(v)); | |
14ba33fb TL |
392 | }); |
393 | }, | |
394 | }, | |
395 | { | |
396 | xtype: 'filebutton', | |
397 | name: 'crypt-upload-button', | |
398 | iconCls: 'fa fa-fw fa-folder-open-o x-btn-icon-el-default-toolbar-small', | |
399 | cls: 'x-btn-default-toolbar-small proxmox-inline-button', | |
400 | margin: '0 0 0 4', | |
401 | disabled: true, | |
402 | hidden: true, | |
403 | listeners: { | |
404 | change: function(btn, e, value) { | |
405 | let ev = e.event; | |
406 | let field = btn.up().down('proxmoxtextfield[name=crypt-key-upload]'); | |
407 | PVE.Utils.loadTextFromFile(ev.target.files[0], v => field.setValue(v)); | |
408 | btn.reset(); | |
409 | }, | |
410 | }, | |
411 | }, | |
412 | ], | |
413 | }, | |
414 | { | |
415 | xtype: 'component', | |
416 | border: false, | |
417 | padding: '5 2', | |
418 | userCls: 'pmx-hint', | |
419 | html: // `<b style="color:red;font-weight:600;">${gettext('Warning')}</b>: ` + | |
420 | `<span class="fa fa-exclamation-triangle" style="color:red;font-size:14px;"></span> ` + | |
421 | gettext('Deleting or replacing the encryption key will break restoring backups created with it!'), | |
422 | hidden: true, | |
423 | bind: { | |
424 | hidden: '{!showDangerousHint}', | |
425 | }, | |
426 | }, | |
427 | ], | |
428 | }); | |
429 | ||
ee19d331 TL |
430 | Ext.define('PVE.storage.PBSInputPanel', { |
431 | extend: 'PVE.panel.StorageBase', | |
432 | ||
903e646d | 433 | onlineHelp: 'storage_pbs', |
ee19d331 | 434 | |
d1a7c6ee TL |
435 | apiCallDone: function(success, response, options) { |
436 | let res = response.result.data; | |
437 | if (!(res && res.config && res.config['encryption-key'])) { | |
438 | return; | |
439 | } | |
440 | let key = res.config['encryption-key']; | |
441 | Ext.create('PVE.Storage.PBSKeyShow', { | |
442 | autoShow: true, | |
443 | sid: res.storage, | |
444 | key: key, | |
445 | }); | |
446 | }, | |
447 | ||
5b7ab402 TL |
448 | isPBS: true, // HACK |
449 | ||
14ba33fb TL |
450 | extraTabs: [ |
451 | { | |
452 | xtype: 'pvePBSEncryptionKeyTab', | |
453 | title: gettext('Encryption'), | |
454 | }, | |
455 | ], | |
456 | ||
ae595bc1 LS |
457 | setValues: function(values) { |
458 | let me = this; | |
459 | ||
460 | let server = values.server; | |
461 | if (values.port !== undefined) { | |
462 | if (Proxmox.Utils.IP6_match.test(server)) { | |
463 | server = `[${server}]`; | |
464 | } | |
465 | server += `:${values.port}`; | |
466 | } | |
467 | values.hostport = server; | |
468 | ||
469 | return me.callParent([values]); | |
470 | }, | |
471 | ||
ee19d331 TL |
472 | initComponent: function() { |
473 | var me = this; | |
474 | ||
475 | me.column1 = [ | |
476 | { | |
ae595bc1 | 477 | xtype: me.isCreate ? 'proxmoxtextfield' : 'displayfield', |
ee19d331 TL |
478 | fieldLabel: gettext('Server'), |
479 | allowBlank: false, | |
ae595bc1 LS |
480 | name: 'hostport', |
481 | submitValue: false, | |
482 | vtype: 'HostPort', | |
483 | listeners: { | |
484 | change: function(field, newvalue) { | |
485 | let server = newvalue; | |
486 | let port; | |
487 | ||
488 | let match = Proxmox.Utils.HostPort_match.exec(newvalue); | |
489 | if (match === null) { | |
490 | match = Proxmox.Utils.HostPortBrackets_match.exec(newvalue); | |
491 | if (match === null) { | |
492 | match = Proxmox.Utils.IP6_dotnotation_match.exec(newvalue); | |
493 | } | |
494 | } | |
495 | ||
496 | if (match !== null) { | |
497 | server = match[1]; | |
498 | if (match[2] !== undefined) { | |
499 | port = match[2]; | |
500 | } | |
501 | } | |
502 | ||
503 | field.up('inputpanel').down('field[name=server]').setValue(server); | |
504 | field.up('inputpanel').down('field[name=port]').setValue(port); | |
505 | }, | |
506 | }, | |
507 | }, | |
508 | { | |
509 | xtype: 'proxmoxtextfield', | |
510 | hidden: true, | |
511 | name: 'server', | |
4b105ec5 | 512 | submitValue: me.isCreate, // it is fixed |
ae595bc1 LS |
513 | }, |
514 | { | |
515 | xtype: 'proxmoxtextfield', | |
516 | hidden: true, | |
517 | deleteEmpty: !me.isCreate, | |
518 | name: 'port', | |
ee19d331 TL |
519 | }, |
520 | { | |
521 | xtype: me.isCreate ? 'textfield' : 'displayfield', | |
522 | name: 'username', | |
523 | value: '', | |
524 | emptyText: gettext('Example') + ': admin@pbs', | |
525 | fieldLabel: gettext('Username'), | |
526 | regex: /\S+@\w+/, | |
527 | regexText: gettext('Example') + ': admin@pbs', | |
528 | allowBlank: false, | |
529 | }, | |
530 | { | |
531 | xtype: me.isCreate ? 'textfield' : 'displayfield', | |
532 | inputType: 'password', | |
533 | name: 'password', | |
534 | value: me.isCreate ? '' : '********', | |
535 | emptyText: me.isCreate ? gettext('None') : '', | |
536 | fieldLabel: gettext('Password'), | |
303dc33f | 537 | allowBlank: false, |
ee19d331 | 538 | }, |
ee19d331 TL |
539 | ]; |
540 | ||
541 | me.column2 = [ | |
ee19d331 TL |
542 | { |
543 | xtype: 'displayfield', | |
544 | name: 'content', | |
545 | value: 'backup', | |
546 | submitValue: true, | |
547 | fieldLabel: gettext('Content'), | |
548 | }, | |
db364c61 TL |
549 | { |
550 | xtype: me.isCreate ? 'textfield' : 'displayfield', | |
551 | name: 'datastore', | |
552 | value: '', | |
553 | fieldLabel: 'Datastore', | |
554 | allowBlank: false, | |
555 | }, | |
26f68e65 TL |
556 | { |
557 | xtype: me.isCreate ? 'textfield' : 'displayfield', | |
558 | name: 'namespace', | |
559 | value: '', | |
560 | emptyText: gettext('Root'), | |
561 | fieldLabel: gettext('Namespace'), | |
562 | allowBlank: true, | |
563 | }, | |
ee19d331 TL |
564 | ]; |
565 | ||
566 | me.columnB = [ | |
567 | { | |
76fa1b43 | 568 | xtype: 'proxmoxtextfield', |
ee19d331 | 569 | name: 'fingerprint', |
76fa1b43 | 570 | value: me.isCreate ? null : undefined, |
ee19d331 | 571 | fieldLabel: gettext('Fingerprint'), |
d3d1e199 | 572 | emptyText: gettext('Server certificate SHA-256 fingerprint, required for self-signed certificates'), |
ee19d331 TL |
573 | regex: /[A-Fa-f0-9]{2}(:[A-Fa-f0-9]{2}){31}/, |
574 | regexText: gettext('Example') + ': AB:CD:EF:...', | |
6c23fbbe | 575 | deleteEmpty: !me.isCreate, |
ee19d331 TL |
576 | allowBlank: true, |
577 | }, | |
578 | ]; | |
579 | ||
580 | me.callParent(); | |
581 | }, | |
582 | }); |