]>
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() { | |
375 | let field = this; | |
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 | }; | |
386 | field.inputEl.on('dragover', cancel); | |
387 | field.inputEl.on('dragenter', cancel); | |
388 | field.inputEl.on('drop', function(ev) { | |
389 | ev = ev.event; | |
390 | if (ev.preventDefault) { | |
391 | ev.preventDefault(); | |
392 | } | |
393 | let files = ev.dataTransfer.files; | |
394 | PVE.Utils.loadTextFromFile(files[0], v => field.setValue(v)); | |
395 | }); | |
396 | }, | |
397 | }, | |
398 | { | |
399 | xtype: 'filebutton', | |
400 | name: 'crypt-upload-button', | |
401 | iconCls: 'fa fa-fw fa-folder-open-o x-btn-icon-el-default-toolbar-small', | |
402 | cls: 'x-btn-default-toolbar-small proxmox-inline-button', | |
403 | margin: '0 0 0 4', | |
404 | disabled: true, | |
405 | hidden: true, | |
406 | listeners: { | |
407 | change: function(btn, e, value) { | |
408 | let ev = e.event; | |
409 | let field = btn.up().down('proxmoxtextfield[name=crypt-key-upload]'); | |
410 | PVE.Utils.loadTextFromFile(ev.target.files[0], v => field.setValue(v)); | |
411 | btn.reset(); | |
412 | }, | |
413 | }, | |
414 | }, | |
415 | ], | |
416 | }, | |
417 | { | |
418 | xtype: 'component', | |
419 | border: false, | |
420 | padding: '5 2', | |
421 | userCls: 'pmx-hint', | |
422 | html: // `<b style="color:red;font-weight:600;">${gettext('Warning')}</b>: ` + | |
423 | `<span class="fa fa-exclamation-triangle" style="color:red;font-size:14px;"></span> ` + | |
424 | gettext('Deleting or replacing the encryption key will break restoring backups created with it!'), | |
425 | hidden: true, | |
426 | bind: { | |
427 | hidden: '{!showDangerousHint}', | |
428 | }, | |
429 | }, | |
430 | ], | |
431 | }); | |
432 | ||
ee19d331 TL |
433 | Ext.define('PVE.storage.PBSInputPanel', { |
434 | extend: 'PVE.panel.StorageBase', | |
435 | ||
903e646d | 436 | onlineHelp: 'storage_pbs', |
ee19d331 | 437 | |
d1a7c6ee TL |
438 | apiCallDone: function(success, response, options) { |
439 | let res = response.result.data; | |
440 | if (!(res && res.config && res.config['encryption-key'])) { | |
441 | return; | |
442 | } | |
443 | let key = res.config['encryption-key']; | |
444 | Ext.create('PVE.Storage.PBSKeyShow', { | |
445 | autoShow: true, | |
446 | sid: res.storage, | |
447 | key: key, | |
448 | }); | |
449 | }, | |
450 | ||
5b7ab402 TL |
451 | isPBS: true, // HACK |
452 | ||
14ba33fb TL |
453 | extraTabs: [ |
454 | { | |
455 | xtype: 'pvePBSEncryptionKeyTab', | |
456 | title: gettext('Encryption'), | |
457 | }, | |
458 | ], | |
459 | ||
ee19d331 TL |
460 | initComponent: function() { |
461 | var me = this; | |
462 | ||
463 | me.column1 = [ | |
464 | { | |
465 | xtype: me.isCreate ? 'textfield' : 'displayfield', | |
466 | name: 'server', | |
467 | value: '', | |
468 | vtype: 'DnsOrIp', | |
469 | fieldLabel: gettext('Server'), | |
470 | allowBlank: false, | |
471 | }, | |
472 | { | |
473 | xtype: me.isCreate ? 'textfield' : 'displayfield', | |
474 | name: 'username', | |
475 | value: '', | |
476 | emptyText: gettext('Example') + ': admin@pbs', | |
477 | fieldLabel: gettext('Username'), | |
478 | regex: /\S+@\w+/, | |
479 | regexText: gettext('Example') + ': admin@pbs', | |
480 | allowBlank: false, | |
481 | }, | |
482 | { | |
483 | xtype: me.isCreate ? 'textfield' : 'displayfield', | |
484 | inputType: 'password', | |
485 | name: 'password', | |
486 | value: me.isCreate ? '' : '********', | |
487 | emptyText: me.isCreate ? gettext('None') : '', | |
488 | fieldLabel: gettext('Password'), | |
303dc33f | 489 | allowBlank: false, |
ee19d331 | 490 | }, |
ee19d331 TL |
491 | ]; |
492 | ||
493 | me.column2 = [ | |
ee19d331 TL |
494 | { |
495 | xtype: 'displayfield', | |
496 | name: 'content', | |
497 | value: 'backup', | |
498 | submitValue: true, | |
499 | fieldLabel: gettext('Content'), | |
500 | }, | |
db364c61 TL |
501 | { |
502 | xtype: me.isCreate ? 'textfield' : 'displayfield', | |
503 | name: 'datastore', | |
504 | value: '', | |
505 | fieldLabel: 'Datastore', | |
506 | allowBlank: false, | |
507 | }, | |
ee19d331 TL |
508 | ]; |
509 | ||
510 | me.columnB = [ | |
511 | { | |
76fa1b43 | 512 | xtype: 'proxmoxtextfield', |
ee19d331 | 513 | name: 'fingerprint', |
76fa1b43 | 514 | value: me.isCreate ? null : undefined, |
ee19d331 | 515 | fieldLabel: gettext('Fingerprint'), |
d3d1e199 | 516 | emptyText: gettext('Server certificate SHA-256 fingerprint, required for self-signed certificates'), |
ee19d331 TL |
517 | regex: /[A-Fa-f0-9]{2}(:[A-Fa-f0-9]{2}){31}/, |
518 | regexText: gettext('Example') + ': AB:CD:EF:...', | |
519 | allowBlank: true, | |
520 | }, | |
521 | ]; | |
522 | ||
523 | me.callParent(); | |
524 | }, | |
525 | }); |