From c6a97d0e37173c3a4e3bd0bf36f8972f40180378 Mon Sep 17 00:00:00 2001 From: Karabaesh Date: Wed, 31 Jul 2019 11:41:22 +0200 Subject: [PATCH 1/6] New feature: Sheet protection --- lib/doc/worksheet.js | 28 +++++++ lib/stream/xlsx/worksheet-writer.js | 6 ++ lib/utils/encryptor.js | 62 +++++++++++++++ .../xform/sheet/sheet-protection-xform.js | 77 +++++++++++++++++++ lib/xlsx/xform/sheet/worksheet-xform.js | 8 ++ .../sheet/sheet-protection-xform.spec.js | 24 ++++++ 6 files changed, 205 insertions(+) create mode 100644 lib/utils/encryptor.js create mode 100644 lib/xlsx/xform/sheet/sheet-protection-xform.js create mode 100644 spec/unit/xlsx/xform/sheet/sheet-protection-xform.spec.js diff --git a/lib/doc/worksheet.js b/lib/doc/worksheet.js index 59dfca326..1d17bb5e4 100644 --- a/lib/doc/worksheet.js +++ b/lib/doc/worksheet.js @@ -9,6 +9,7 @@ const Column = require('./column'); const Enums = require('./enums'); const Image = require('./image'); const DataValidations = require('./data-validations'); +const Encryptor = require('../utils/encryptor'); // Worksheet requirements // Operate as sheet inside workbook or standalone @@ -109,6 +110,9 @@ class Worksheet { // for images, etc this._media = []; + + // worksheet protection + this.sheetProtection = {}; } get workbook() { @@ -577,6 +581,28 @@ class Worksheet { return image && image.imageId; } + // ========================================================================= + // Worksheet Protection + protect(password, options){ + if (password) { + this.sheetProtection.algorithmName = 'SHA-512'; + this.sheetProtection.saltValue = Encryptor.randomBytes(16).toString('base64'); + this.sheetProtection.spinCount = 100000; + this.sheetProtection.hashValue = Encryptor.convertPasswordToHash(password, 'SHA512', this.sheetProtection.saltValue, this.sheetProtection.spinCount); + } + this.sheetProtection = Object.assign(this.sheetProtection, {sheet: 1, selectUnlockedCells: 0, selectLockedCells: 1}); + if(options){ + this.sheetProtection = Object.assign(this.sheetProtection, options); + } + } + + unprotect(){ + this.sheetProtection.algorithmName = undefined; + this.sheetProtection.saltValue = undefined; + this.sheetProtection.spinCount = undefined; + this.sheetProtection.hashValue = undefined; + } + // =========================================================================== // Deprecated get tabColor() { @@ -607,6 +633,7 @@ class Worksheet { views: this.views, autoFilter: this.autoFilter, media: this._media.map(medium => medium.model), + sheetProtection: this.sheetProtection, }; // ================================================= @@ -663,6 +690,7 @@ class Worksheet { this.views = value.views; this.autoFilter = value.autoFilter; this._media = value.media.map(medium => new Image(this, medium)); + this.sheetProtection = value.sheetProtection; } } diff --git a/lib/stream/xlsx/worksheet-writer.js b/lib/stream/xlsx/worksheet-writer.js index 62087738f..6c415308d 100644 --- a/lib/stream/xlsx/worksheet-writer.js +++ b/lib/stream/xlsx/worksheet-writer.js @@ -27,6 +27,7 @@ const ColXform = require('../../xlsx/xform/sheet/col-xform'); const RowXform = require('../../xlsx/xform/sheet/row-xform'); const HyperlinkXform = require('../../xlsx/xform/sheet/hyperlink-xform'); const SheetViewXform = require('../../xlsx/xform/sheet/sheet-view-xform'); +const SheetProtectionXform = require('../../xlsx/xform/sheet/sheet-protection-xform'); const PageMarginsXform = require('../../xlsx/xform/sheet/page-margins-xform'); const PageSetupXform = require('../../xlsx/xform/sheet/page-setup-xform'); const AutoFilterXform = require('../../xlsx/xform/sheet/auto-filter-xform'); @@ -41,6 +42,7 @@ const xform = { row: new RowXform(), hyperlinks: new ListXform({ tag: 'hyperlinks', length: false, childXform: new HyperlinkXform() }), sheetViews: new ListXform({ tag: 'sheetViews', length: false, childXform: new SheetViewXform() }), + sheetProtection: new SheetProtectionXform(), pageMargins: new PageMarginsXform(), pageSeteup: new PageSetupXform(), autoFilter: new AutoFilterXform(), @@ -210,6 +212,7 @@ WorksheetWriter.prototype = { this._writeHyperlinks(); this._writeDataValidations(); + this._writeSheetProtection(); this._writePageMargins(); this._writePageSetup(); this._writeBackground(); @@ -518,6 +521,9 @@ WorksheetWriter.prototype = { _writeDataValidations() { this.stream.write(xform.dataValidations.toXml(this.dataValidations.model)); }, + _writeSheetProtection() { + this.stream.write(xform.sheetProtection.toXml(this.sheetProtection)); + }, _writePageMargins() { this.stream.write(xform.pageMargins.toXml(this.pageSetup.margins)); }, diff --git a/lib/utils/encryptor.js b/lib/utils/encryptor.js new file mode 100644 index 000000000..a24d16f41 --- /dev/null +++ b/lib/utils/encryptor.js @@ -0,0 +1,62 @@ +'use strict'; + +const crypto = require('crypto'); +const buffer1 = require('buffer'); + +const Encryptor = { + /** + * Calculate a hash of the concatenated buffers with the given algorithm. + * @param {string} algorithm - The hash algorithm. + * @param {Array.} buffers - The buffers to concat and hash + * @returns {Buffer} The hash + */ + hash(algorithm) { + const buffers = []; + for (let i = 1; i < arguments.length; i++) { + buffers[i - 1] = arguments[i]; + } + algorithm = algorithm.toLowerCase(); + const hashes = crypto.getHashes(); + if (hashes.indexOf(algorithm) < 0) { + throw new Error(`Hash algorithm '${ algorithm }' not supported!`); + } + const hash = crypto.createHash(algorithm); + hash.update(buffer1.Buffer.concat(buffers)); + return hash.digest(); + }, + /** + * Convert a password into an encryption key + * @param {string} password - The password + * @param {string} hashAlgorithm - The hash algoritm + * @param {string} saltValue - The salt value + * @param {number} spinCount - The spin count + * @param {number} keyBits - The length of the key in bits + * @param {Buffer} blockKey - The block key + * @returns {Buffer} The encryption key + */ + convertPasswordToHash(password, hashAlgorithm, saltValue, spinCount) { + // Password must be in unicode buffer + const passwordBuffer = buffer1.Buffer.from(password, 'utf16le'); + // Generate the initial hash + let key = this.hash( + hashAlgorithm, + buffer1.Buffer.from(saltValue, 'base64'), + passwordBuffer + ); + // Now regenerate until spin count + for (let i = 0; i < spinCount; i++) { + const iterator = buffer1.Buffer.alloc(4); + iterator.writeUInt32LE(i, 0); + key = this.hash(hashAlgorithm, key, iterator); + } + return key.toString('base64'); + }, + /** + * Generates cryptographically strong pseudo-random data. + * @param size The size argument is a number indicating the number of bytes to generate. + */ + randomBytes(size) { + return crypto.randomBytes(size); + }, +}; +module.exports = Encryptor; diff --git a/lib/xlsx/xform/sheet/sheet-protection-xform.js b/lib/xlsx/xform/sheet/sheet-protection-xform.js new file mode 100644 index 000000000..85e7f7f81 --- /dev/null +++ b/lib/xlsx/xform/sheet/sheet-protection-xform.js @@ -0,0 +1,77 @@ +const _ = require('../../../utils/under-dash'); +const BaseXform = require('../base-xform'); + +class SheetProtectionXform extends BaseXform { + get tag() { + return 'sheetProtection'; + } + + render(xmlStream, model) { + if (model) { + const attributes = { + algorithmName: model.algorithmName, + hashValue: model.hashValue, + saltValue: model.saltValue, + spinCount: model.spinCount, + sheet: model.sheet, + objects: model.objects, + scenarios: model.scenarios, + selectLockedCells: model.selectLockedCells, + selectUnlockedCells: model.selectUnlockedCells, + formatCells: model.formatCells, + formatColumns: model.formatColumns, + formatRows: model.formatRows, + insertColumns: model.insertColumns, + insertRows: model.insertRows, + insertHyperlinks: model.insertHyperlinks, + deleteColumns: model.deleteColumns, + deleteRows: model.deleteRows, + sort: model.sort, + autoFilter: model.autoFilter, + pivotTables: model.pivotTables, + }; + if (_.some(attributes, value => value !== undefined)) { + xmlStream.leafNode(this.tag, attributes); + } + } + } + + parseOpen(node) { + switch (node.name) { + case this.tag: + this.model = { + algorithmName: node.attributes.algorithmName, + hashValue: node.attributes.hashValue, + saltValue: node.attributes.saltValue, + spinCount: node.attributes.spinCount, + sheet: parseInt(node.attributes.sheet || '1', 10), + objects: parseInt(node.attributes.objects || '1', 10), + scenarios: parseInt(node.attributes.scenarios || '1', 10), + selectLockedCells: parseInt(node.attributes.selectLockedCells || '0', 10), + selectUnlockedCells: parseInt(node.attributes.selectUnlockedCells || '0', 10), + formatCells: parseInt(node.attributes.formatCells || '1', 10), + formatColumns: parseInt(node.attributes.formatColumns || '1', 10), + formatRows: parseInt(node.attributes.formatRows || '1', 10), + insertColumns: parseInt(node.attributes.insertColumns || '1', 10), + insertRows: parseInt(node.attributes.insertRows || '1', 10), + insertHyperlinks: parseInt(node.attributes.insertHyperlinks || '1', 10), + deleteColumns: parseInt(node.attributes.deleteColumns || '1', 10), + deleteRows: parseInt(node.attributes.deleteRows || '1', 10), + sort: parseInt(node.attributes.sort || '1', 10), + autoFilter: parseInt(node.attributes.autoFilter || '1', 10), + pivotTables: parseInt(node.attributes.pivotTables || '1', 10), + }; + return true; + default: + return false; + } + } + + parseText() {} + + parseClose() { + return false; + } +} + +module.exports = SheetProtectionXform; diff --git a/lib/xlsx/xform/sheet/worksheet-xform.js b/lib/xlsx/xform/sheet/worksheet-xform.js index 956241db0..3e6044393 100644 --- a/lib/xlsx/xform/sheet/worksheet-xform.js +++ b/lib/xlsx/xform/sheet/worksheet-xform.js @@ -18,6 +18,7 @@ const DataValidationsXform = require('./data-validations-xform'); const SheetPropertiesXform = require('./sheet-properties-xform'); const SheetFormatPropertiesXform = require('./sheet-format-properties-xform'); const SheetViewXform = require('./sheet-view-xform'); +const SheetProtectionXform = require('./sheet-protection-xform'); const PageMarginsXform = require('./page-margins-xform'); const PageSetupXform = require('./page-setup-xform'); const PrintOptionsXform = require('./print-options-xform'); @@ -56,6 +57,7 @@ class WorkSheetXform extends BaseXform { printOptions: new PrintOptionsXform(), picture: new PictureXform(), drawing: new DrawingXform(), + sheetProtection: new SheetProtectionXform(), }; } @@ -195,6 +197,7 @@ class WorkSheetXform extends BaseXform { horizontalCentered: model.horizontalCentered, verticalCentered: model.verticalCentered, }; + const sheetProtectionModel = model.sheetProtection; this.map.sheetPr.render(xmlStream, sheetPropertiesModel); this.map.dimension.render(xmlStream, model.dimensions); @@ -202,6 +205,7 @@ class WorkSheetXform extends BaseXform { this.map.sheetFormatPr.render(xmlStream, sheetFormatPropertiesModel); this.map.cols.render(xmlStream, model.cols); this.map.sheetData.render(xmlStream, model.rows); + this.map.sheetProtection.render(xmlStream, sheetProtectionModel); // Note: must be after sheetData and before autoFilter this.map.autoFilter.render(xmlStream, model.autoFilter); this.map.mergeCells.render(xmlStream, model.mergeCells); this.map.dataValidations.render(xmlStream, model.dataValidations); @@ -291,6 +295,10 @@ class WorkSheetXform extends BaseXform { if (this.map.autoFilter.model) { this.model.autoFilter = this.map.autoFilter.model; } + if (this.map.sheetProtection.model) { + this.model.sheetProtection = this.map.sheetProtection.model; + } + return false; } diff --git a/spec/unit/xlsx/xform/sheet/sheet-protection-xform.spec.js b/spec/unit/xlsx/xform/sheet/sheet-protection-xform.spec.js new file mode 100644 index 000000000..ea20ff5b1 --- /dev/null +++ b/spec/unit/xlsx/xform/sheet/sheet-protection-xform.spec.js @@ -0,0 +1,24 @@ +'use strict'; + +const SheetProtectionXform = require('../../../../../lib/xlsx/xform/sheet/sheet-protection-xform'); +const testXformHelper = require('../test-xform-helper'); + +const expectations = [ + { + title: 'Normal', + create() { + return new SheetProtectionXform(); + }, + preparedModel: { + }, + xml: + '', + parsedModel: { + }, + tests: ['render', 'renderIn', 'parse'], + }, +]; + +describe('SheetProtectionXform', () => { + testXformHelper(expectations); +}); From f6468be669010a4b31c2ac2bb14cfb55cc1923c0 Mon Sep 17 00:00:00 2001 From: Karabaesh Date: Wed, 31 Jul 2019 14:27:38 +0200 Subject: [PATCH 2/6] Additional unit tests --- index.d.ts | 24 +++++ lib/doc/worksheet.js | 11 ++- .../xform/sheet/sheet-protection-xform.js | 92 +++++++++++-------- spec/unit/utils/encryptor.spec.js | 14 +++ .../sheet/sheet-protection-xform.spec.js | 72 ++++++++++++++- 5 files changed, 166 insertions(+), 47 deletions(-) create mode 100644 spec/unit/utils/encryptor.spec.js diff --git a/index.d.ts b/index.d.ts index d96f04b3b..938ab6baa 100644 --- a/index.d.ts +++ b/index.d.ts @@ -811,6 +811,24 @@ export type AutoFilter = string | { to: string | { row: number; column: number }; }; +export interface WorksheetProtection { + objects: boolean; + scenarios: boolean; + selectLockedCells: boolean; + selectUnlockedCells: boolean; + formatCells: boolean; + formatColumns: boolean; + formatRows: boolean; + insertColumns: boolean; + insertRows: boolean; + insertHyperlinks: boolean; + deleteColumns: boolean; + deleteRows: boolean; + sort: boolean; + autoFilter: boolean; + pivotTables: boolean; +} + export interface Image { extension: 'jpeg' | 'png' | 'gif'; base64?: string; @@ -1124,6 +1142,12 @@ export interface Worksheet { commit(): void; model: WorksheetModel; + + /** + * Worksheet protection + */ + protect(password: string, options: Partial); + unprotect(); } export interface WorksheetProperties { diff --git a/lib/doc/worksheet.js b/lib/doc/worksheet.js index 1d17bb5e4..24d401177 100644 --- a/lib/doc/worksheet.js +++ b/lib/doc/worksheet.js @@ -584,23 +584,24 @@ class Worksheet { // ========================================================================= // Worksheet Protection protect(password, options){ + this.sheetProtection.sheet = true; if (password) { this.sheetProtection.algorithmName = 'SHA-512'; this.sheetProtection.saltValue = Encryptor.randomBytes(16).toString('base64'); this.sheetProtection.spinCount = 100000; this.sheetProtection.hashValue = Encryptor.convertPasswordToHash(password, 'SHA512', this.sheetProtection.saltValue, this.sheetProtection.spinCount); } - this.sheetProtection = Object.assign(this.sheetProtection, {sheet: 1, selectUnlockedCells: 0, selectLockedCells: 1}); if(options){ this.sheetProtection = Object.assign(this.sheetProtection, options); } + if(this.sheetProtection.selectLockedCells === false && this.sheetProtection.selectUnlockedCells){ + this.sheetProtection.objects = true; + this.sheetProtection.scenarios = true; + } } unprotect(){ - this.sheetProtection.algorithmName = undefined; - this.sheetProtection.saltValue = undefined; - this.sheetProtection.spinCount = undefined; - this.sheetProtection.hashValue = undefined; + this.sheetProtection.sheet = undefined; } // =========================================================================== diff --git a/lib/xlsx/xform/sheet/sheet-protection-xform.js b/lib/xlsx/xform/sheet/sheet-protection-xform.js index 85e7f7f81..2a887f398 100644 --- a/lib/xlsx/xform/sheet/sheet-protection-xform.js +++ b/lib/xlsx/xform/sheet/sheet-protection-xform.js @@ -1,6 +1,14 @@ const _ = require('../../../utils/under-dash'); const BaseXform = require('../base-xform'); +function booleanToXml(model, value) { + return model ? value : undefined; +} + +function xmlToBoolean(value, equals) { + return value === equals ? true : undefined; +} + class SheetProtectionXform extends BaseXform { get tag() { return 'sheetProtection'; @@ -9,27 +17,29 @@ class SheetProtectionXform extends BaseXform { render(xmlStream, model) { if (model) { const attributes = { - algorithmName: model.algorithmName, - hashValue: model.hashValue, - saltValue: model.saltValue, - spinCount: model.spinCount, - sheet: model.sheet, - objects: model.objects, - scenarios: model.scenarios, - selectLockedCells: model.selectLockedCells, - selectUnlockedCells: model.selectUnlockedCells, - formatCells: model.formatCells, - formatColumns: model.formatColumns, - formatRows: model.formatRows, - insertColumns: model.insertColumns, - insertRows: model.insertRows, - insertHyperlinks: model.insertHyperlinks, - deleteColumns: model.deleteColumns, - deleteRows: model.deleteRows, - sort: model.sort, - autoFilter: model.autoFilter, - pivotTables: model.pivotTables, + sheet: booleanToXml(model.sheet, '1'), + selectLockedCells: model.selectLockedCells === false ? '1' : undefined, + selectUnlockedCells: model.selectUnlockedCells === false ? '1' : undefined, + formatCells: booleanToXml(model.formatCells, '0'), + formatColumns: booleanToXml(model.formatColumns, '0'), + formatRows: booleanToXml(model.formatRows, '0'), + insertColumns: booleanToXml(model.insertColumns, '0'), + insertRows: booleanToXml(model.insertRows, '0'), + insertHyperlinks: booleanToXml(model.insertHyperlinks, '0'), + deleteColumns: booleanToXml(model.deleteColumns, '0'), + deleteRows: booleanToXml(model.deleteRows, '0'), + sort: booleanToXml(model.sort, '0'), + autoFilter: booleanToXml(model.autoFilter, '0'), + pivotTables: booleanToXml(model.pivotTables, '0'), }; + if(model.sheet){ + attributes.algorithmName = model.algorithmName; + attributes.hashValue = model.hashValue; + attributes.saltValue = model.saltValue; + attributes.spinCount = model.spinCount; + attributes.objects = booleanToXml(model.objects === false, '1'); + attributes.scenarios = booleanToXml(model.scenarios === false, '1'); + } if (_.some(attributes, value => value !== undefined)) { xmlStream.leafNode(this.tag, attributes); } @@ -40,27 +50,29 @@ class SheetProtectionXform extends BaseXform { switch (node.name) { case this.tag: this.model = { - algorithmName: node.attributes.algorithmName, - hashValue: node.attributes.hashValue, - saltValue: node.attributes.saltValue, - spinCount: node.attributes.spinCount, - sheet: parseInt(node.attributes.sheet || '1', 10), - objects: parseInt(node.attributes.objects || '1', 10), - scenarios: parseInt(node.attributes.scenarios || '1', 10), - selectLockedCells: parseInt(node.attributes.selectLockedCells || '0', 10), - selectUnlockedCells: parseInt(node.attributes.selectUnlockedCells || '0', 10), - formatCells: parseInt(node.attributes.formatCells || '1', 10), - formatColumns: parseInt(node.attributes.formatColumns || '1', 10), - formatRows: parseInt(node.attributes.formatRows || '1', 10), - insertColumns: parseInt(node.attributes.insertColumns || '1', 10), - insertRows: parseInt(node.attributes.insertRows || '1', 10), - insertHyperlinks: parseInt(node.attributes.insertHyperlinks || '1', 10), - deleteColumns: parseInt(node.attributes.deleteColumns || '1', 10), - deleteRows: parseInt(node.attributes.deleteRows || '1', 10), - sort: parseInt(node.attributes.sort || '1', 10), - autoFilter: parseInt(node.attributes.autoFilter || '1', 10), - pivotTables: parseInt(node.attributes.pivotTables || '1', 10), + sheet: xmlToBoolean(node.attributes.sheet, '1'), + objects: (node.attributes.objects === '1') ? false : undefined, + scenarios: (node.attributes.scenarios === '1') ? false : undefined, + selectLockedCells: (node.attributes.selectLockedCells === '1') ? false : undefined, + selectUnlockedCells: (node.attributes.selectUnlockedCells === '1') ? false : undefined, + formatCells: xmlToBoolean(node.attributes.formatCells, '0'), + formatColumns: xmlToBoolean(node.attributes.formatColumns, '0'), + formatRows: xmlToBoolean(node.attributes.formatRows, '0'), + insertColumns: xmlToBoolean(node.attributes.insertColumns, '0'), + insertRows: xmlToBoolean(node.attributes.insertRows, '0'), + insertHyperlinks: xmlToBoolean(node.attributes.insertHyperlinks, '0'), + deleteColumns: xmlToBoolean(node.attributes.deleteColumns, '0'), + deleteRows: xmlToBoolean(node.attributes.deleteRows, '0'), + sort: xmlToBoolean(node.attributes.sort, '0'), + autoFilter: xmlToBoolean(node.attributes.autoFilter, '0'), + pivotTables: xmlToBoolean(node.attributes.pivotTables, '0'), }; + if(node.attributes.algorithmName){ + this.model.algorithmName = node.attributes.algorithmName; + this.model.hashValue = node.attributes.hashValue; + this.model.saltValue = node.attributes.saltValue; + this.model.spinCount = parseInt(node.attributes.spinCount, 10); + } return true; default: return false; diff --git a/spec/unit/utils/encryptor.spec.js b/spec/unit/utils/encryptor.spec.js new file mode 100644 index 000000000..680488d63 --- /dev/null +++ b/spec/unit/utils/encryptor.spec.js @@ -0,0 +1,14 @@ +const { expect } = require('chai'); + +const Encryptor = require('../../../lib/utils/encryptor'); + +describe('Encryptor', () => { + it('Stores and shares string values', () => { + const password = '123'; + const saltValue = '6tC6yotbNa8JaMaDvbUgxw=='; + const spinCount = 100000; + const hash = Encryptor.convertPasswordToHash(password, 'SHA512',saltValue, spinCount); + expect(hash).to.equal('RHtx1KpAYT7nBzGCTInkHrbf2wTZxP3BT4Eo8PBHPTM4KfKArJTluFvizDvo6GnBCOO6JJu7qwKvMqnKHs7dcw=='); + }); + +}); diff --git a/spec/unit/xlsx/xform/sheet/sheet-protection-xform.spec.js b/spec/unit/xlsx/xform/sheet/sheet-protection-xform.spec.js index ea20ff5b1..1905e81de 100644 --- a/spec/unit/xlsx/xform/sheet/sheet-protection-xform.spec.js +++ b/spec/unit/xlsx/xform/sheet/sheet-protection-xform.spec.js @@ -5,18 +5,86 @@ const testXformHelper = require('../test-xform-helper'); const expectations = [ { - title: 'Normal', + title: 'Unprotected (Empty)', + create() { + return new SheetProtectionXform(); + }, + preparedModel: {}, + xml: '', + parsedModel: {}, + tests: ['render', 'renderIn'], + }, + { + title: 'Protected (Default)', + create() { + return new SheetProtectionXform(); + }, + preparedModel: { + algorithmName: 'SHA-512', + hashValue: 'RHtx1KpAYT7nBzGCTInkHrbf2wTZxP3BT4Eo8PBHPTM4KfKArJTluFvizDvo6GnBCOO6JJu7qwKvMqnKHs7dcw==', + saltValue: '6tC6yotbNa8JaMaDvbUgxw==', + spinCount: 100000, + sheet: true, + objects: false, + scenarios: false, + }, + xml: + '', + parsedModel: { + algorithmName: 'SHA-512', + hashValue: 'RHtx1KpAYT7nBzGCTInkHrbf2wTZxP3BT4Eo8PBHPTM4KfKArJTluFvizDvo6GnBCOO6JJu7qwKvMqnKHs7dcw==', + saltValue: '6tC6yotbNa8JaMaDvbUgxw==', + spinCount: 100000, + sheet: true, + objects: false, + scenarios: false, + }, + tests: ['render', 'renderIn', 'parse'], + }, + { + title: 'Unprotected (All false)', create() { return new SheetProtectionXform(); }, preparedModel: { + selectLockedCells: false, + selectUnlockedCells: false, }, xml: - '', + '', parsedModel: { + selectLockedCells: false, + selectUnlockedCells: false, }, tests: ['render', 'renderIn', 'parse'], }, + { + title: 'Protected (All false)', + create() { + return new SheetProtectionXform(); + }, + preparedModel: { + algorithmName: 'SHA-512', + hashValue: 'RHtx1KpAYT7nBzGCTInkHrbf2wTZxP3BT4Eo8PBHPTM4KfKArJTluFvizDvo6GnBCOO6JJu7qwKvMqnKHs7dcw==', + saltValue: '6tC6yotbNa8JaMaDvbUgxw==', + spinCount: 100000, + sheet: true, + selectLockedCells: false, + selectUnlockedCells: false, + }, + xml: + '', + parsedModel: { + algorithmName: 'SHA-512', + hashValue: 'RHtx1KpAYT7nBzGCTInkHrbf2wTZxP3BT4Eo8PBHPTM4KfKArJTluFvizDvo6GnBCOO6JJu7qwKvMqnKHs7dcw==', + saltValue: '6tC6yotbNa8JaMaDvbUgxw==', + spinCount: 100000, + sheet: true, + selectLockedCells: false, + selectUnlockedCells: false, + }, + tests: ['render', 'renderIn', 'parse'], + }, ]; describe('SheetProtectionXform', () => { From 1d9c4818ff3a54bed6cfba2adff6d094304d972f Mon Sep 17 00:00:00 2001 From: Karabaesh Date: Wed, 31 Jul 2019 15:10:23 +0200 Subject: [PATCH 3/6] Fix sheet protection options Signed-off-by: Karabaesh --- lib/doc/worksheet.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/doc/worksheet.js b/lib/doc/worksheet.js index 0a3017b97..ca9596d11 100644 --- a/lib/doc/worksheet.js +++ b/lib/doc/worksheet.js @@ -597,10 +597,6 @@ class Worksheet { if(options){ this.sheetProtection = Object.assign(this.sheetProtection, options); } - if(this.sheetProtection.selectLockedCells === false && this.sheetProtection.selectUnlockedCells){ - this.sheetProtection.objects = true; - this.sheetProtection.scenarios = true; - } } unprotect(){ From 3f02ae4000168a0e75f0416b88f4071705f256cb Mon Sep 17 00:00:00 2001 From: Karabaesh Date: Wed, 31 Jul 2019 16:02:38 +0200 Subject: [PATCH 4/6] Fix unit test 'encryptor' --- spec/unit/utils/encryptor.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/unit/utils/encryptor.spec.js b/spec/unit/utils/encryptor.spec.js index 680488d63..958c7bf46 100644 --- a/spec/unit/utils/encryptor.spec.js +++ b/spec/unit/utils/encryptor.spec.js @@ -3,11 +3,11 @@ const { expect } = require('chai'); const Encryptor = require('../../../lib/utils/encryptor'); describe('Encryptor', () => { - it('Stores and shares string values', () => { + it('Generates SHA-512 hash for given password, salt value and spin count', () => { const password = '123'; const saltValue = '6tC6yotbNa8JaMaDvbUgxw=='; const spinCount = 100000; - const hash = Encryptor.convertPasswordToHash(password, 'SHA512',saltValue, spinCount); + const hash = Encryptor.convertPasswordToHash(password, 'SHA512', saltValue, spinCount); expect(hash).to.equal('RHtx1KpAYT7nBzGCTInkHrbf2wTZxP3BT4Eo8PBHPTM4KfKArJTluFvizDvo6GnBCOO6JJu7qwKvMqnKHs7dcw=='); }); From fdf201d6fec5a5f5dbe8080666ad7cec6645c0f2 Mon Sep 17 00:00:00 2001 From: Karabaesh Date: Tue, 13 Aug 2019 13:02:32 +0200 Subject: [PATCH 5/6] Included requests of guyonroche; Wrapped sheet protection into promise --- lib/doc/worksheet.js | 34 +++++++++++++++-------------- lib/stream/xlsx/worksheet-writer.js | 27 ++++++++++++----------- lib/utils/encryptor.js | 10 ++++----- 3 files changed, 36 insertions(+), 35 deletions(-) diff --git a/lib/doc/worksheet.js b/lib/doc/worksheet.js index ca9596d11..a3678b5d8 100644 --- a/lib/doc/worksheet.js +++ b/lib/doc/worksheet.js @@ -9,6 +9,7 @@ const Image = require('./image'); const Table= require('./table'); const DataValidations = require('./data-validations'); const Encryptor = require('../utils/encryptor'); +const PromiseLib = require('../utils/promise'); // Worksheet requirements // Operate as sheet inside workbook or standalone @@ -111,7 +112,7 @@ class Worksheet { this._media = []; // worksheet protection - this.sheetProtection = {}; + this.sheetProtection = null; // for tables this.tables = {}; @@ -587,24 +588,25 @@ class Worksheet { // ========================================================================= // Worksheet Protection protect(password, options){ - this.sheetProtection.sheet = true; - if (password) { - this.sheetProtection.algorithmName = 'SHA-512'; - this.sheetProtection.saltValue = Encryptor.randomBytes(16).toString('base64'); - this.sheetProtection.spinCount = 100000; - this.sheetProtection.hashValue = Encryptor.convertPasswordToHash(password, 'SHA512', this.sheetProtection.saltValue, this.sheetProtection.spinCount); - } - if(options){ - this.sheetProtection = Object.assign(this.sheetProtection, options); - } + return new PromiseLib.Promise(resolve => { + this.sheetProtection = { + sheet : true, + }; + if (password) { + this.sheetProtection.algorithmName = 'SHA-512'; + this.sheetProtection.saltValue = Encryptor.randomBytes(16).toString('base64'); + this.sheetProtection.spinCount = 100000; + this.sheetProtection.hashValue = Encryptor.convertPasswordToHash(password, 'SHA512', this.sheetProtection.saltValue, this.sheetProtection.spinCount); + } + if(options){ + this.sheetProtection = Object.assign(this.sheetProtection, options); + } + resolve(); + }); } unprotect(){ - this.sheetProtection.sheet = undefined; - this.sheetProtection.algorithmName = undefined; - this.sheetProtection.saltValue = undefined; - this.sheetProtection.spinCount = undefined; - this.sheetProtection.hashValue = undefined; + this.sheetProtection = null; } // ========================================================================= diff --git a/lib/stream/xlsx/worksheet-writer.js b/lib/stream/xlsx/worksheet-writer.js index 6c415308d..db6805eee 100644 --- a/lib/stream/xlsx/worksheet-writer.js +++ b/lib/stream/xlsx/worksheet-writer.js @@ -212,19 +212,20 @@ WorksheetWriter.prototype = { this._writeHyperlinks(); this._writeDataValidations(); - this._writeSheetProtection(); - this._writePageMargins(); - this._writePageSetup(); - this._writeBackground(); - this._writeCloseWorksheet(); - - // signal end of stream to workbook - this.stream.end(); - - // also commit the hyperlinks if any - this._sheetRelsWriter.commit(); - - this.committed = true; + this._writeSheetProtection().then(() =>{ + this._writePageMargins(); + this._writePageSetup(); + this._writeBackground(); + this._writeCloseWorksheet(); + + // signal end of stream to workbook + this.stream.end(); + + // also commit the hyperlinks if any + this._sheetRelsWriter.commit(); + + this.committed = true; + }); }, // return the current dimensions of the writer diff --git a/lib/utils/encryptor.js b/lib/utils/encryptor.js index a24d16f41..8923164c1 100644 --- a/lib/utils/encryptor.js +++ b/lib/utils/encryptor.js @@ -1,13 +1,11 @@ 'use strict'; const crypto = require('crypto'); -const buffer1 = require('buffer'); const Encryptor = { /** * Calculate a hash of the concatenated buffers with the given algorithm. * @param {string} algorithm - The hash algorithm. - * @param {Array.} buffers - The buffers to concat and hash * @returns {Buffer} The hash */ hash(algorithm) { @@ -21,7 +19,7 @@ const Encryptor = { throw new Error(`Hash algorithm '${ algorithm }' not supported!`); } const hash = crypto.createHash(algorithm); - hash.update(buffer1.Buffer.concat(buffers)); + hash.update(Buffer.concat(buffers)); return hash.digest(); }, /** @@ -36,16 +34,16 @@ const Encryptor = { */ convertPasswordToHash(password, hashAlgorithm, saltValue, spinCount) { // Password must be in unicode buffer - const passwordBuffer = buffer1.Buffer.from(password, 'utf16le'); + const passwordBuffer = Buffer.from(password, 'utf16le'); // Generate the initial hash let key = this.hash( hashAlgorithm, - buffer1.Buffer.from(saltValue, 'base64'), + Buffer.from(saltValue, 'base64'), passwordBuffer ); // Now regenerate until spin count for (let i = 0; i < spinCount; i++) { - const iterator = buffer1.Buffer.alloc(4); + const iterator = Buffer.alloc(4); iterator.writeUInt32LE(i, 0); key = this.hash(hashAlgorithm, key, iterator); } From f575957c86ffb34bbce289fbd0a380dc19ebbef3 Mon Sep 17 00:00:00 2001 From: Karabaesh Date: Wed, 14 Aug 2019 11:36:15 +0200 Subject: [PATCH 6/6] Changes requested by guyonroche concerning the hasing algorithm --- lib/stream/xlsx/worksheet-writer.js | 27 +++++++++++++-------------- lib/utils/encryptor.js | 10 +++------- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/lib/stream/xlsx/worksheet-writer.js b/lib/stream/xlsx/worksheet-writer.js index db6805eee..6c415308d 100644 --- a/lib/stream/xlsx/worksheet-writer.js +++ b/lib/stream/xlsx/worksheet-writer.js @@ -212,20 +212,19 @@ WorksheetWriter.prototype = { this._writeHyperlinks(); this._writeDataValidations(); - this._writeSheetProtection().then(() =>{ - this._writePageMargins(); - this._writePageSetup(); - this._writeBackground(); - this._writeCloseWorksheet(); - - // signal end of stream to workbook - this.stream.end(); - - // also commit the hyperlinks if any - this._sheetRelsWriter.commit(); - - this.committed = true; - }); + this._writeSheetProtection(); + this._writePageMargins(); + this._writePageSetup(); + this._writeBackground(); + this._writeCloseWorksheet(); + + // signal end of stream to workbook + this.stream.end(); + + // also commit the hyperlinks if any + this._sheetRelsWriter.commit(); + + this.committed = true; }, // return the current dimensions of the writer diff --git a/lib/utils/encryptor.js b/lib/utils/encryptor.js index 8923164c1..b87da598d 100644 --- a/lib/utils/encryptor.js +++ b/lib/utils/encryptor.js @@ -8,12 +8,7 @@ const Encryptor = { * @param {string} algorithm - The hash algorithm. * @returns {Buffer} The hash */ - hash(algorithm) { - const buffers = []; - for (let i = 1; i < arguments.length; i++) { - buffers[i - 1] = arguments[i]; - } - algorithm = algorithm.toLowerCase(); + hash(algorithm, ...buffers) { const hashes = crypto.getHashes(); if (hashes.indexOf(algorithm) < 0) { throw new Error(`Hash algorithm '${ algorithm }' not supported!`); @@ -33,6 +28,7 @@ const Encryptor = { * @returns {Buffer} The encryption key */ convertPasswordToHash(password, hashAlgorithm, saltValue, spinCount) { + hashAlgorithm = hashAlgorithm.toLowerCase(); // Password must be in unicode buffer const passwordBuffer = Buffer.from(password, 'utf16le'); // Generate the initial hash @@ -43,7 +39,7 @@ const Encryptor = { ); // Now regenerate until spin count for (let i = 0; i < spinCount; i++) { - const iterator = Buffer.alloc(4); + const iterator = Buffer.alloc(4); iterator.writeUInt32LE(i, 0); key = this.hash(hashAlgorithm, key, iterator); } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy