/*global QRCode*/
-Ext.define('Proxmox.form.PBSEncryptionCheckbox', {
- extend: 'Ext.form.field.Checkbox',
- xtype: 'pbsEncryptionCheckbox',
-
- inputValue: true,
-
- viewModel: {
- data: {
- value: null,
- originalValue: null,
- },
- formulas: {
- blabel: (get) => {
- let v = get('value');
- let original = get('originalValue');
- if (!get('isCreate') && original) {
- if (!v) {
- return gettext('Warning: Existing encryption key will be deleted!');
- }
- return gettext('Active');
- } else {
- return gettext('Auto-generate a client encryption key, saved privately on cluster filesystem');
- }
- },
- },
- },
-
- bind: {
- value: '{value}',
- boxLabel: '{blabel}',
- },
- resetOriginalValue: function() {
- let me = this;
- let vm = me.getViewModel();
- vm.set('originalValue', me.value);
-
- me.callParent(arguments);
- },
-
- 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' };
- }
- } else if (val) {
- return { 'encryption-key': 'autogen' };
- }
- return null;
- },
-
- initComponent: function() {
- let me = this;
- me.callParent();
-
- let vm = me.getViewModel();
- vm.set('isCreate', me.isCreate);
- },
-});
-
Ext.define('PVE.Storage.PBSKeyShow', {
extend: 'Ext.window.Window',
- alias: ['widget.pveKeyShow'],
+ xtype: 'pvePBSKeyShow',
mixins: ['Proxmox.Mixin.CBind'],
width: 600,
{
xtype: 'textfield',
fieldLabel: gettext('Key'),
- labelWidth: 30,
+ labelWidth: 80,
inputId: 'encryption-key-value',
cbind: {
value: '{key}',
},
{
xtype: 'component',
- html: gettext('Keep your master key safe, but easily accessible for disaster recovery.')
+ html: gettext('Keep your encryption key safe, but easily accessible for disaster recovery.')
+ '<br>' + gettext('We recommend the following safe-keeping strategy:'),
},
{
border: false,
padding: '10 10 10 10',
userCls: 'pmx-hint',
- html: gettext('Please save the encryption key - loosing it will render any backup created with it unuseable'),
+ html: gettext('Please save the encryption key - losing it will render any backup created with it unusable'),
},
],
buttons: [
},
},
],
- paperkey: function(key) {
+ paperkey: function(keyString) {
let me = this;
+ const key = JSON.parse(keyString);
+
const qrwidth = 500;
let qrdiv = document.createElement('div');
let qrcode = new QRCode(qrdiv, {
height: qrwidth,
correctLevel: QRCode.CorrectLevel.H,
});
- qrcode.makeCode(key);
+ 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, {
height: "0",
border: "0",
});
- const prettifiedKey = JSON.stringify(JSON.parse(key), null, 2);
+ 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}'</h4>
-<p style="font-size: 1.2em; font-family: monospace; white-space: pre-wrap;">
+ <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>
printFrame.src = "data:text/html;base64," + btoa(html);
document.body.appendChild(printFrame);
+ me.on('destroy', () => document.body.removeChild(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 = '<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 (key.data === undefined) {
+ return "Does not seems like a valid Proxmox Backup key!";
+ }
+ }
+ return true;
+ },
+ afterRender: function() {
+ if (!window.FileReader) {
+ // No FileReader support in this browser
+ return;
+ }
+ let cancel = function(ev) {
+ ev = ev.event;
+ if (ev.preventDefault) {
+ ev.preventDefault();
+ }
+ };
+ this.inputEl.on('dragover', cancel);
+ this.inputEl.on('dragenter', cancel);
+ this.inputEl.on('drop', ev => {
+ cancel(ev);
+ let files = ev.event.dataTransfer.files;
+ PVE.Utils.loadTextFromFile(files[0], v => this.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;
});
},
+ isPBS: true, // HACK
+
+ extraTabs: [
+ {
+ xtype: 'pvePBSEncryptionKeyTab',
+ title: gettext('Encryption'),
+ },
+ ],
+
+ setValues: function(values) {
+ let me = this;
+
+ let server = values.server;
+ if (values.port !== undefined) {
+ if (Proxmox.Utils.IP6_match.test(server)) {
+ server = `[${server}]`;
+ }
+ server += `:${values.port}`;
+ }
+ values.hostport = server;
+
+ return me.callParent([values]);
+ },
+
initComponent: function() {
var me = this;
me.column1 = [
{
- xtype: me.isCreate ? 'textfield' : 'displayfield',
- name: 'server',
- value: '',
- vtype: 'DnsOrIp',
+ xtype: me.isCreate ? 'proxmoxtextfield' : 'displayfield',
fieldLabel: gettext('Server'),
allowBlank: false,
+ name: 'hostport',
+ submitValue: false,
+ vtype: 'HostPort',
+ listeners: {
+ change: function(field, newvalue) {
+ let server = newvalue;
+ let port;
+
+ let match = Proxmox.Utils.HostPort_match.exec(newvalue);
+ if (match === null) {
+ match = Proxmox.Utils.HostPortBrackets_match.exec(newvalue);
+ if (match === null) {
+ match = Proxmox.Utils.IP6_dotnotation_match.exec(newvalue);
+ }
+ }
+
+ if (match !== null) {
+ server = match[1];
+ if (match[2] !== undefined) {
+ port = match[2];
+ }
+ }
+
+ field.up('inputpanel').down('field[name=server]').setValue(server);
+ field.up('inputpanel').down('field[name=port]').setValue(port);
+ },
+ },
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ hidden: true,
+ name: 'server',
+ submitValue: me.isCreate, // it is fixed
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ hidden: true,
+ deleteEmpty: !me.isCreate,
+ name: 'port',
},
{
xtype: me.isCreate ? 'textfield' : 'displayfield',
fieldLabel: gettext('Password'),
allowBlank: false,
},
- {
- xtype: me.isCreate ? 'textfield' : 'displayfield',
- name: 'datastore',
- value: '',
- fieldLabel: 'Datastore',
- allowBlank: false,
- },
];
me.column2 = [
submitValue: true,
fieldLabel: gettext('Content'),
},
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'datastore',
+ value: '',
+ fieldLabel: 'Datastore',
+ allowBlank: false,
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'namespace',
+ value: '',
+ emptyText: gettext('Root'),
+ fieldLabel: gettext('Namespace'),
+ allowBlank: true,
+ },
];
me.columnB = [
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:...',
+ deleteEmpty: !me.isCreate,
allowBlank: true,
},
- {
- // FIXME: allow uploading their own, maybe export for root@pam?
- xtype: 'pbsEncryptionCheckbox',
- name: 'encryption-key',
- isCreate: me.isCreate,
- fieldLabel: gettext('Encryption Key'),
- },
];
me.callParent();