]> git.proxmox.com Git - pve-manager.git/blob - www/manager6/storage/PBSEdit.js
ui: acme: add External Account Binding (EAB) related fields
[pve-manager.git] / www / manager6 / storage / PBSEdit.js
1 /*global QRCode*/
2 Ext.define('PVE.Storage.PBSKeyShow', {
3 extend: 'Ext.window.Window',
4 xtype: 'pvePBSKeyShow',
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'),
34 labelWidth: 80,
35 inputId: 'encryption-key-value',
36 cbind: {
37 value: '{key}',
38 },
39 editable: false,
40 },
41 {
42 xtype: 'component',
43 html: gettext('Keep your encryption key safe, but easily accessible for disaster recovery.')
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',
127 html: gettext('Please save the encryption key - losing it will render any backup created with it unusable'),
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 ],
139 paperkey: function(keyString) {
140 let me = this;
141
142 const key = JSON.parse(keyString);
143
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 });
151 qrcode.makeCode(keyString);
152
153 let shortKeyFP = '';
154 if (key.fingerprint) {
155 shortKeyFP = PVE.Utils.render_pbs_fingerprint(key.fingerprint);
156 }
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 });
167 const prettifiedKey = JSON.stringify(key, null, 2);
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;">
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;">
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 me.on('destroy', () => document.body.removeChild(printFrame));
185 },
186 });
187
188 Ext.define('PVE.panel.PBSEncryptionKeyTab', {
189 extend: 'Proxmox.panel.InputPanel',
190 xtype: 'pvePBSEncryptionKeyTab',
191 mixins: ['Proxmox.Mixin.CBind'],
192
193 onlineHelp: 'storage_pbs_encryption',
194
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) {
215 let icon = '<span class="fa fa-lock good"></span> ';
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 }
369 if (key.data === undefined) {
370 return "Does not seems like a valid Proxmox Backup key!";
371 }
372 }
373 return true;
374 },
375 afterRender: function() {
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 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));
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
430 Ext.define('PVE.storage.PBSInputPanel', {
431 extend: 'PVE.panel.StorageBase',
432
433 onlineHelp: 'storage_pbs',
434
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
448 isPBS: true, // HACK
449
450 extraTabs: [
451 {
452 xtype: 'pvePBSEncryptionKeyTab',
453 title: gettext('Encryption'),
454 },
455 ],
456
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
472 initComponent: function() {
473 var me = this;
474
475 me.column1 = [
476 {
477 xtype: me.isCreate ? 'proxmoxtextfield' : 'displayfield',
478 fieldLabel: gettext('Server'),
479 allowBlank: false,
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',
512 submitValue: me.isCreate, // it is fixed
513 },
514 {
515 xtype: 'proxmoxtextfield',
516 hidden: true,
517 deleteEmpty: !me.isCreate,
518 name: 'port',
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'),
537 allowBlank: false,
538 },
539 ];
540
541 me.column2 = [
542 {
543 xtype: 'displayfield',
544 name: 'content',
545 value: 'backup',
546 submitValue: true,
547 fieldLabel: gettext('Content'),
548 },
549 {
550 xtype: me.isCreate ? 'textfield' : 'displayfield',
551 name: 'datastore',
552 value: '',
553 fieldLabel: 'Datastore',
554 allowBlank: false,
555 },
556 {
557 xtype: me.isCreate ? 'textfield' : 'displayfield',
558 name: 'namespace',
559 value: '',
560 emptyText: gettext('Root'),
561 fieldLabel: gettext('Namespace'),
562 allowBlank: true,
563 },
564 ];
565
566 me.columnB = [
567 {
568 xtype: 'proxmoxtextfield',
569 name: 'fingerprint',
570 value: me.isCreate ? null : undefined,
571 fieldLabel: gettext('Fingerprint'),
572 emptyText: gettext('Server certificate SHA-256 fingerprint, required for self-signed certificates'),
573 regex: /[A-Fa-f0-9]{2}(:[A-Fa-f0-9]{2}){31}/,
574 regexText: gettext('Example') + ': AB:CD:EF:...',
575 deleteEmpty: !me.isCreate,
576 allowBlank: true,
577 },
578 ];
579
580 me.callParent();
581 },
582 });