Skip to content

Feature/Sheet Protection #907

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Aug 15, 2019
Merged
24 changes: 24 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,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;
Expand Down Expand Up @@ -1129,6 +1147,12 @@ export interface Worksheet {
commit(): void;

model: WorksheetModel;

/**
* Worksheet protection
*/
protect(password: string, options: Partial<WorksheetProtection>);
unprotect();
}

export interface WorksheetProperties {
Expand Down
32 changes: 32 additions & 0 deletions lib/doc/worksheet.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ const Enums = require('./enums');
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
Expand Down Expand Up @@ -109,8 +111,12 @@ class Worksheet {
// for images, etc
this._media = [];

// worksheet protection
this.sheetProtection = null;

// for tables
this.tables = {};

}

get workbook() {
Expand Down Expand Up @@ -579,6 +585,30 @@ class Worksheet {
return image && image.imageId;
}

// =========================================================================
// Worksheet Protection
protect(password, 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 = null;
}

// =========================================================================
// Tables
addTable(model) {
Expand Down Expand Up @@ -629,6 +659,7 @@ class Worksheet {
views: this.views,
autoFilter: this.autoFilter,
media: this._media.map(medium => medium.model),
sheetProtection: this.sheetProtection,
tables: Object.values(this.tables).map(table => table.model),
};

Expand Down Expand Up @@ -686,6 +717,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;
this.tables = value.tables.reduce(
(tables, table) => {
const t = new Table;
Expand Down
6 changes: 6 additions & 0 deletions lib/stream/xlsx/worksheet-writer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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(),
Expand Down Expand Up @@ -210,6 +212,7 @@ WorksheetWriter.prototype = {

this._writeHyperlinks();
this._writeDataValidations();
this._writeSheetProtection();
this._writePageMargins();
this._writePageSetup();
this._writeBackground();
Expand Down Expand Up @@ -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));
},
Expand Down
56 changes: 56 additions & 0 deletions lib/utils/encryptor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
'use strict';

const crypto = require('crypto');

const Encryptor = {
/**
* Calculate a hash of the concatenated buffers with the given algorithm.
* @param {string} algorithm - The hash algorithm.
* @returns {Buffer} The hash
*/
hash(algorithm, ...buffers) {
const hashes = crypto.getHashes();
if (hashes.indexOf(algorithm) < 0) {
throw new Error(`Hash algorithm '${ algorithm }' not supported!`);
}
const hash = crypto.createHash(algorithm);
hash.update(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) {
hashAlgorithm = hashAlgorithm.toLowerCase();
// Password must be in unicode buffer
const passwordBuffer = Buffer.from(password, 'utf16le');
// Generate the initial hash
let key = this.hash(
hashAlgorithm,
Buffer.from(saltValue, 'base64'),
passwordBuffer
);
// Now regenerate until spin count
for (let i = 0; i < spinCount; i++) {
const iterator = 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;
89 changes: 89 additions & 0 deletions lib/xlsx/xform/sheet/sheet-protection-xform.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
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';
}

render(xmlStream, model) {
if (model) {
const attributes = {
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);
}
}
}

parseOpen(node) {
switch (node.name) {
case this.tag:
this.model = {
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;
}
}

parseText() {}

parseClose() {
return false;
}
}

module.exports = SheetProtectionXform;
8 changes: 8 additions & 0 deletions lib/xlsx/xform/sheet/worksheet-xform.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -57,6 +58,7 @@ class WorkSheetXform extends BaseXform {
printOptions: new PrintOptionsXform(),
picture: new PictureXform(),
drawing: new DrawingXform(),
sheetProtection: new SheetProtectionXform(),
tableParts: new ListXform({tag: 'tableParts', count: true, childXform: new TablePartXform()}),
};
}
Expand Down Expand Up @@ -217,13 +219,15 @@ 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);
this.map.sheetViews.render(xmlStream, model.views);
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);
Expand Down Expand Up @@ -317,6 +321,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;
}

Expand Down
14 changes: 14 additions & 0 deletions spec/unit/utils/encryptor.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const { expect } = require('chai');

const Encryptor = require('../../../lib/utils/encryptor');

describe('Encryptor', () => {
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);
expect(hash).to.equal('RHtx1KpAYT7nBzGCTInkHrbf2wTZxP3BT4Eo8PBHPTM4KfKArJTluFvizDvo6GnBCOO6JJu7qwKvMqnKHs7dcw==');
});

});
Loading
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