/*global QRCode*/ Ext.define('PVE.Storage.PBSKeyShow', { extend: 'Ext.window.Window', xtype: 'pvePBSKeyShow', mixins: ['Proxmox.Mixin.CBind'], width: 600, modal: true, resizable: false, title: gettext('Important: Save your Encryption Key'), // avoid that esc closes this by mistake, force user to more manual action onEsc: Ext.emptyFn, closable: false, items: [ { xtype: 'form', layout: { type: 'vbox', align: 'stretch', }, bodyPadding: 10, border: false, defaults: { anchor: '100%', border: false, padding: '10 0 0 0', }, items: [ { xtype: 'textfield', fieldLabel: gettext('Key'), labelWidth: 80, inputId: 'encryption-key-value', cbind: { value: '{key}', }, editable: false, }, { xtype: 'component', html: gettext('Keep your encryption key safe, but easily accessible for disaster recovery.') + '
' + gettext('We recommend the following safe-keeping strategy:'), }, { xtyp: 'container', layout: 'hbox', items: [ { xtype: 'component', html: '1. ' + gettext('Save the key in your password manager.'), flex: 1, }, { xtype: 'button', text: gettext('Copy Key'), iconCls: 'fa fa-clipboard x-btn-icon-el-default-toolbar-small', cls: 'x-btn-default-toolbar-small proxmox-inline-button', width: 110, handler: function(b) { document.getElementById('encryption-key-value').select(); document.execCommand("copy"); }, }, ], }, { xtype: 'container', layout: 'hbox', items: [ { xtype: 'component', html: '2. ' + gettext('Download the key to a USB (pen) drive, placed in secure vault.'), flex: 1, }, { xtype: 'button', text: gettext('Download'), iconCls: 'fa fa-download x-btn-icon-el-default-toolbar-small', cls: 'x-btn-default-toolbar-small proxmox-inline-button', width: 110, handler: function(b) { let win = this.up('window'); let pveID = PVE.ClusterName || window.location.hostname; let name = `pve-${pveID}-storage-${win.sid}.enc`; let hiddenElement = document.createElement('a'); hiddenElement.href = 'data:attachment/text,' + encodeURI(win.key); hiddenElement.target = '_blank'; hiddenElement.download = name; hiddenElement.click(); }, }, ], }, { xtype: 'container', layout: 'hbox', items: [ { xtype: 'component', html: '3. ' + gettext('Print as paperkey, laminated and placed in secure vault.'), flex: 1, }, { xtype: 'button', text: gettext('Print Key'), iconCls: 'fa fa-print x-btn-icon-el-default-toolbar-small', cls: 'x-btn-default-toolbar-small proxmox-inline-button', width: 110, handler: function(b) { let win = this.up('window'); win.paperkey(win.key); }, }, ], }, ], }, { xtype: 'component', border: false, padding: '10 10 10 10', userCls: 'pmx-hint', html: gettext('Please save the encryption key - losing it will render any backup created with it unusable'), }, ], buttons: [ { text: gettext('Close'), handler: function(b) { let win = this.up('window'); win.close(); }, }, ], paperkey: function(keyString) { let me = this; const key = JSON.parse(keyString); const qrwidth = 500; let qrdiv = document.createElement('div'); let qrcode = new QRCode(qrdiv, { width: qrwidth, height: qrwidth, correctLevel: QRCode.CorrectLevel.H, }); qrcode.makeCode(keyString); let shortKeyFP = ''; if (key.fingerprint) { shortKeyFP = PVE.Utils.render_pbs_fingerprint(key.fingerprint); } let printFrame = document.createElement("iframe"); Object.assign(printFrame.style, { position: "fixed", right: "0", bottom: "0", width: "0", height: "0", border: "0", }); const prettifiedKey = JSON.stringify(key, null, 2); const keyQrBase64 = qrdiv.children[0].toDataURL("image/png"); const html = `

Encryption Key - Storage '${me.sid}' (${shortKeyFP})

-----BEGIN PROXMOX BACKUP KEY----- ${prettifiedKey} -----END PROXMOX BACKUP KEY-----

`; printFrame.src = "data:text/html;base64," + btoa(html); document.body.appendChild(printFrame); }, }); Ext.define('PVE.panel.PBSEncryptionKeyTab', { extend: 'Proxmox.panel.InputPanel', xtype: 'pvePBSEncryptionKeyTab', mixins: ['Proxmox.Mixin.CBind'], onlineHelp: 'storage_pbs_encryption', onGetValues: function(form) { let values = {}; if (form.cryptMode === 'upload') { values['encryption-key'] = form['crypt-key-upload']; } else if (form.cryptMode === 'autogenerate') { values['encryption-key'] = 'autogen'; } else if (form.cryptMode === 'none') { if (!this.isCreate) { values.delete = ['encryption-key']; } } return values; }, setValues: function(values) { let me = this; let vm = me.getViewModel(); let cryptKeyInfo = values['encryption-key']; if (cryptKeyInfo) { let icon = ' '; if (cryptKeyInfo.match(/^[a-fA-F0-9]{2}:/)) { // new style fingerprint let shortKeyFP = PVE.Utils.render_pbs_fingerprint(cryptKeyInfo); values['crypt-key-fp'] = icon + `${gettext('Active')} - ${gettext('Fingerprint')} ${shortKeyFP}`; } else { // old key without FP values['crypt-key-fp'] = icon + gettext('Active'); } } else { values['crypt-key-fp'] = gettext('None'); let cryptModeNone = me.down('radiofield[inputValue=none]'); cryptModeNone.setBoxLabel(gettext('Do not encrypt backups')); cryptModeNone.setValue(true); } vm.set('keepCryptVisible', !!cryptKeyInfo); vm.set('allowEdit', !cryptKeyInfo); me.callParent([values]); }, viewModel: { data: { allowEdit: true, keepCryptVisible: false, }, formulas: { showDangerousHint: get => { let allowEdit = get('allowEdit'); return get('keepCryptVisible') && allowEdit; }, }, }, items: [ { xtype: 'displayfield', name: 'crypt-key-fp', fieldLabel: gettext('Encryption Key'), padding: '2 0', }, { xtype: 'checkbox', name: 'crypt-allow-edit', boxLabel: gettext('Edit existing encryption key (dangerous!)'), hidden: true, submitValue: false, isDirty: () => false, bind: { hidden: '{!keepCryptVisible}', value: '{allowEdit}', }, }, { xtype: 'radiofield', name: 'cryptMode', inputValue: 'keep', boxLabel: gettext('Keep encryption key'), padding: '0 0 0 25', cbind: { hidden: '{isCreate}', checked: '{!isCreate}', }, bind: { hidden: '{!keepCryptVisible}', disabled: '{!allowEdit}', }, }, { xtype: 'radiofield', name: 'cryptMode', inputValue: 'none', checked: true, padding: '0 0 0 25', cbind: { disabled: '{!isCreate}', checked: '{isCreate}', boxLabel: get => get('isCreate') ? gettext('Do not encrypt backups') : gettext('Delete existing encryption key'), }, bind: { disabled: '{!allowEdit}', }, }, { xtype: 'radiofield', name: 'cryptMode', inputValue: 'autogenerate', boxLabel: gettext('Auto-generate a client encryption key'), padding: '0 0 0 25', cbind: { disabled: '{!isCreate}', }, bind: { disabled: '{!allowEdit}', }, }, { xtype: 'radiofield', name: 'cryptMode', inputValue: 'upload', boxLabel: gettext('Upload an existing client encryption key'), padding: '0 0 0 25', cbind: { disabled: '{!isCreate}', }, bind: { disabled: '{!allowEdit}', }, listeners: { change: function(f, value) { let panel = this.up('inputpanel'); if (!panel.rendered) { return; } let uploadKeyField = panel.down('field[name=crypt-key-upload]'); uploadKeyField.setDisabled(!value); uploadKeyField.setHidden(!value); let uploadKeyButton = panel.down('filebutton[name=crypt-upload-button]'); uploadKeyButton.setDisabled(!value); uploadKeyButton.setHidden(!value); if (value) { uploadKeyField.validate(); } else { uploadKeyField.reset(); } }, }, }, { xtype: 'fieldcontainer', layout: 'hbox', items: [ { xtype: 'proxmoxtextfield', name: 'crypt-key-upload', fieldLabel: gettext('Key'), value: '', disabled: true, hidden: true, allowBlank: false, labelAlign: 'right', flex: 1, emptyText: gettext('You can drag-and-drop a key file here.'), validator: function(value) { if (value.length) { let key; try { key = JSON.parse(value); } catch (e) { return "Failed to parse key - " + e; } if (typeof key.data === undefined) { return "Does not seems like a valid Proxmox Backup key!"; } } return true; }, afterRender: function() { let field = this; if (!window.FileReader) { // No FileReader support in this browser return; } let cancel = function(ev) { ev = ev.event; if (ev.preventDefault) { ev.preventDefault(); } }; field.inputEl.on('dragover', cancel); field.inputEl.on('dragenter', cancel); field.inputEl.on('drop', function(ev) { ev = ev.event; if (ev.preventDefault) { ev.preventDefault(); } let files = ev.dataTransfer.files; PVE.Utils.loadTextFromFile(files[0], v => field.setValue(v)); }); }, }, { xtype: 'filebutton', name: 'crypt-upload-button', iconCls: 'fa fa-fw fa-folder-open-o x-btn-icon-el-default-toolbar-small', cls: 'x-btn-default-toolbar-small proxmox-inline-button', margin: '0 0 0 4', disabled: true, hidden: true, listeners: { change: function(btn, e, value) { let ev = e.event; let field = btn.up().down('proxmoxtextfield[name=crypt-key-upload]'); PVE.Utils.loadTextFromFile(ev.target.files[0], v => field.setValue(v)); btn.reset(); }, }, }, ], }, { xtype: 'component', border: false, padding: '5 2', userCls: 'pmx-hint', html: // `${gettext('Warning')}: ` + ` ` + gettext('Deleting or replacing the encryption key will break restoring backups created with it!'), hidden: true, bind: { hidden: '{!showDangerousHint}', }, }, ], }); Ext.define('PVE.storage.PBSInputPanel', { extend: 'PVE.panel.StorageBase', onlineHelp: 'storage_pbs', apiCallDone: function(success, response, options) { let res = response.result.data; if (!(res && res.config && res.config['encryption-key'])) { return; } let key = res.config['encryption-key']; Ext.create('PVE.Storage.PBSKeyShow', { autoShow: true, sid: res.storage, key: key, }); }, isPBS: true, // HACK extraTabs: [ { xtype: 'pvePBSEncryptionKeyTab', title: gettext('Encryption'), }, ], initComponent: function() { var me = this; me.column1 = [ { xtype: me.isCreate ? 'textfield' : 'displayfield', name: 'server', value: '', vtype: 'DnsOrIp', fieldLabel: gettext('Server'), allowBlank: false, }, { xtype: me.isCreate ? 'textfield' : 'displayfield', name: 'username', value: '', emptyText: gettext('Example') + ': admin@pbs', fieldLabel: gettext('Username'), regex: /\S+@\w+/, regexText: gettext('Example') + ': admin@pbs', allowBlank: false, }, { xtype: me.isCreate ? 'textfield' : 'displayfield', inputType: 'password', name: 'password', value: me.isCreate ? '' : '********', emptyText: me.isCreate ? gettext('None') : '', fieldLabel: gettext('Password'), allowBlank: false, }, ]; me.column2 = [ { xtype: 'displayfield', name: 'content', value: 'backup', submitValue: true, fieldLabel: gettext('Content'), }, { xtype: me.isCreate ? 'textfield' : 'displayfield', name: 'datastore', value: '', fieldLabel: 'Datastore', allowBlank: false, }, ]; me.columnB = [ { xtype: 'proxmoxtextfield', name: 'fingerprint', value: me.isCreate ? null : undefined, fieldLabel: gettext('Fingerprint'), emptyText: gettext('Server certificate SHA-256 fingerprint, required for self-signed certificates'), regex: /[A-Fa-f0-9]{2}(:[A-Fa-f0-9]{2}){31}/, regexText: gettext('Example') + ': AB:CD:EF:...', allowBlank: true, }, ]; me.callParent(); }, });