-Ext.define('Proxmox.form.PBSEncryptionCheckbox', {
- extend: 'Ext.form.field.Checkbox',
- xtype: 'pbsEncryptionCheckbox',
+/*global QRCode*/
+Ext.define('PVE.Storage.PBSKeyShow', {
+ extend: 'Ext.window.Window',
+ xtype: 'pvePBSKeyShow',
+ mixins: ['Proxmox.Mixin.CBind'],
- inputValue: true,
+ width: 600,
+ modal: true,
+ resizable: false,
+ title: gettext('Important: Save your Encryption Key'),
- viewModel: {
- data: {
- value: null,
- originalValue: null,
+ // 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.')
+ + '<br>' + 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);
+ },
+ },
+ ],
+ },
+ ],
},
- formulas: {
- blabel: (get) => {
- let v = get('value');
- let original = get('originalValue');
- if (get('isCreate')) {
- return gettext('Auto-generate a client encryption key, safed privately on cluster.');
- }
- if (original) {
- if (!v) {
- return gettext('Warning: Existing encryption Key will be deleted!');
- }
- return gettext('Active');
- } else {
- return gettext('Auto-generate a client encryption key, safed privately on cluster filesystem');
- }
+ {
+ 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();
},
},
- },
-
- bind: {
- value: '{value}',
- boxLabel: '{blabel}',
- },
- resetOriginalValue: function() {
+ ],
+ paperkey: function(keyString) {
let me = this;
- let vm = me.getViewModel();
- vm.set('originalValue', me.value);
- me.callParent(arguments);
+ 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 = `<html><head><script>
+ window.addEventListener('DOMContentLoaded', (ev) => window.print());
+ </script><style>@media print and (max-height: 150mm) {
+ h4, p { margin: 0; font-size: 1em; }
+ }</style></head><body style="padding: 5px;">
+ <h4>Encryption Key - Storage '${me.sid}' (${shortKeyFP})</h4>
+<p style="font-size:1.2em;font-family:monospace;white-space:pre-wrap;overflow-wrap:break-word;">
+-----BEGIN PROXMOX BACKUP KEY-----
+${prettifiedKey}
+-----END PROXMOX BACKUP KEY-----</p>
+ <center><img style="width: 100%; max-width: ${qrwidth}px;" src="${keyQrBase64}"></center>
+ </body></html>`;
+
+ printFrame.src = "data:text/html;base64," + btoa(html);
+ document.body.appendChild(printFrame);
},
+});
- getSubmitData: function() {
- let me = this;
- let val = me.getSubmitValue();
- if (!me.isCreate) {
- if (val === null) {
- return { 'delete': 'encryption-key' };
- } else if (val && !!val !== !!me.originalValue) {
- return { 'encryption-key': 'autogen' };
+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'];
}
- } else if (val) {
- return { 'encryption-key': 'autogen' };
}
- return null;
+ return values;
},
- initComponent: function() {
+ setValues: function(values) {
let me = this;
- me.callParent();
-
let vm = me.getViewModel();
- vm.set('isCreate', me.isCreate);
+
+ let cryptKeyInfo = values['encryption-key'];
+ if (cryptKeyInfo) {
+ let icon = '<span class="fa fa-lock good"></span> ';
+ 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: // `<b style="color:red;font-weight:600;">${gettext('Warning')}</b>: ` +
+ `<span class="fa fa-exclamation-triangle" style="color:red;font-size:14px;"></span> ` +
+ 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',
+ 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;
fieldLabel: gettext('Password'),
allowBlank: false,
},
- {
- xtype: me.isCreate ? 'textfield' : 'displayfield',
- name: 'datastore',
- value: '',
- fieldLabel: 'Datastore',
- allowBlank: false,
- },
];
me.column2 = [
- { // FIXME: prune settings
- xtype: 'proxmoxintegerfield',
- fieldLabel: gettext('Max Backups'),
- name: 'maxfiles',
- reference: 'maxfiles',
- minValue: 0,
- maxValue: 365,
- value: me.isCreate ? '0' : undefined,
- allowBlank: false,
- },
{
xtype: 'displayfield',
name: 'content',
submitValue: true,
fieldLabel: gettext('Content'),
},
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'datastore',
+ value: '',
+ fieldLabel: 'Datastore',
+ allowBlank: false,
+ },
];
me.columnB = [
{
- xtype: 'textfield',
+ xtype: 'proxmoxtextfield',
name: 'fingerprint',
- value: me.isCreate ? '' : undefined,
+ 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,
},
- {
- // FIXME: allow uploading their own, maybe export for root@pam?
- xtype: 'pbsEncryptionCheckbox',
- name: 'encryption-key',
- isCreate: me.isCreate,
- fieldLabel: gettext('Encryption Key'),
- },
- {
- xtype: 'displayfield',
- userCls: 'pmx-hint',
- value: `Proxmox Backup Server is currently in beta.`,
- },
];
me.callParent();