npm: crypto-es
在前端和后端项目中,加密,解密和哈希的需求非常普遍。 处理敏感数据时,在代码中可能会经常看到名称为MD5
, Base64
或AES
类的函数。
在所有JavaScript密码库中, CryptoJS因其简单的API和丰富的功能而被广泛使用。 它起源于早期,主要代码库仍在Google代码上。 尽管已迁移到npm,但最近一次更新是3年前。 由于历史原因,它的某些功能对于现代用法似乎已经过时了:
- 它基于原型继承创建了一个独特的面向对象的系统,以模拟类继承。
- 这些文件使用立即调用函数表达式(IIFE)导出模块。
在ECMAScript 6之前,这些功能避免了JavaScript的某些缺陷,同时确保了浏览器的兼容性和开箱即用的功能。 但是,由于新的ECMAScript标准已经指定了Class和Module系统来解决这些缺陷,因此,我将尝试在现代ECMAScript中以实验方式重构CryptoJS。
该项目称为CryptoES 。 作为实验,兼容性不是首要考虑的问题,唯一的规则是满足最新的ECMAScript标准。 例如,我只将ECMAScript模块用于模块系统,而没有CommonJS支持。 借助Babel和装载机挂钩,该项目可以满足任何生产应用。 随着ECMAScript标准的普及,它的使用将更加直接。
此外,作为一个重构项目,它的API与CryptoJS相同。
重构为ECMAScript类
CryptoJS扩展了JavaScript原型继承并实现了一个独特的“基础对象继承”系统。 它具有扩展,覆盖,混合等功能,使其类似于基于通用类的OOP语言。 这在方法上有所不同,但在目的上与ECMAScript类相同。 我们的第一步是用类替换这个唯一的继承系统,这将使代码简洁。 但是在此之前,让我们回顾一下ECMAScript类的一些关键概念:
1.构造函数
我们知道,在ECMAScript中,类定义是传统JavaScript构造函数的语法糖。 因此,我们可以通过实例的构造函数字段访问实例的类。 我们可以用这些知识做什么? 在实例的实例方法中,我们可以通过表达new this.constructor()
来创建一个与旧实例具有相同类的新实例。
2.这个
与传统的JavaScript原型继承不同,在ECMAScript类继承中,在创建实例时,将首先调用其超类的构造函数,然后将超类的字段和方法添加到实例中,最后是超类的构造函数。实例将被调用。 这样可以确保子类中定义的字段和方法将正确覆盖超类。
请注意,实例方法中的this引用实例,而静态方法中的this引用类。 因此,我们可以通过在类的静态方法中调用new this()来实现工厂模式。
3.超级
在类定义中,带括号的super()
指的是超类的构造函数,而在静态方法中不带括号的super就是这样,指的是超类。 因此,在覆盖方法时,我们可以先通过表达式super.overridedMethod.call(this)
调用超类的覆盖方法。
4.原型&__proto__
类本质上是一个构造函数,因此它具有一个引用其原型对象的原型字段。 一个类也是一个对象,因此它也有一个__proto__
字段,它也引用了它的超类。 由于类的原型具有__proto__
字段,该字段引用它的超类的原型对象,因此构成了原型链。
实例的__proto__
字段引用其类的原型对象。
使用这些功能,我们可以获得实例和类的继承关系。
如今,常用的CryptoJS版本托管在GitHub上: brix / crypto-js , CryptoES将主要基于此版本。 它的“基础对象”在core.js文件中定义。
继承在名为Base的对象中实现:
var Base = C_lib.Base = ( function ( ) {
return {
extend : function ( overrides ) {
// Spawn
var subtype = create( this );
// Augment
if (overrides) {
subtype.mixIn(overrides);
}
// Create default initializer
if (!subtype.hasOwnProperty( 'init' ) || this .init === subtype.init) {
subtype.init = function ( ) {
subtype.$ super .init.apply( this , arguments );
};
}
// Initializer's prototype is the subtype object
subtype.init.prototype = subtype;
// Reference supertype
subtype.$ super = this ;
return subtype;
},
create: function ( ) {
var instance = this .extend();
instance.init.apply(instance, arguments );
return instance;
},
init: function ( ) {
},
};
}());
实际上,继承是通过使用覆盖字段和方法的参数调用“超级对象”的extend方法来实现的,并且extend方法会创建一个新的“子对象”。 实际实例将由此“子对象”的create方法返回,其中的init方法将被称为构造函数。 这种方法的缺点是,实例的创建不是使用关键字new常规创建的。 实例将递归地将其继承链的所有“祖先对象”保留在$ super字段中,这肯定是多余的。
使用ECMAScript类的关键字extend和类构造函数可以轻松实现这些目标,而无需上面的任何额外代码或问题。 但是为了保证API的一致性,将保留静态方法create,以便您可以使用ClassName.create()创建实例。 create方法的参数将由rest运算符传递给实际的构造函数,并且参数销毁:
export class Base {
static create(...args) {
return new this (...args);
}
}
Base的基本目的是提供mixin方法。 在CryptoJS中,诸如基础对象和实际实例对象之类的“类对象”之间的边界是模糊的,并且可以通过“类对象”来调用mixin方法:
mixIn: function ( properties ) {
for ( var propertyName in properties) {
if (properties.hasOwnProperty(propertyName)) {
this [propertyName] = properties[propertyName];
}
}
// IE won't copy toString using the loop above
if (properties.hasOwnProperty( 'toString' )) {
this .toString = properties.toString;
}
},
但实际上,mixin方法应该是一个实例方法,其作用类似于Object.assign() :
mixIn(properties) {
return Object .assign( this , properties);
}
最后,复制实例。 使用原型继承,CryptoJS实现很奇怪:
clone() {
const clone = new this .constructor();
Object .assign(clone, this );
return clone;
}
由于新的this.constructor()可以在任何实例中使用,而无需指明它是类名,因此我们可以采用更直接的方法来实现:
clone() {
const clone = new this .constructor();
Object .assign(clone, this );
return clone;
}
然后,通过继承Base类,所有其他核心类将具有这些方法。 借助ECMAScript类,方法的使用将更加标准化。 例如,在CryptoJS中,Wordarray的一些实例创建方法是:
var WordArray = C_lib.WordArray = Base.extend({
init : function ( words, sigBytes ) {
words = this .words = words || [];
if (sigBytes != undefined ) {
this .sigBytes = sigBytes;
} else {
this .sigBytes = words.length * 4 ;
}
},
random : function ( nBytes ) {
...
return new WordArray.init(words, nBytes);
}
});
But now they are standard constructor or static methods:
export class WordArray extends Base {
constructor (words = [], sigBytes = words.length * 4) {
super ();
this .words = words;
this .sigBytes = sigBytes;
}
static random(nBytes) {
...
return new WordArray(words, nBytes);
}
}
重构为类之后,我们应该为继承关系添加一些额外的单元测试:
__Proto__
的子类必须是指超class.prototype
子类和超类的对象是在原型链中正确的顺序的。
子类和超类的原型对象在原型链中的顺序正确。
data.Obj = class Obj extends C . lib . Base {
};
data.obj = data.Obj.create();
it( 'class inheritance' , () => {
expect(data.Obj.__proto__).toBe(C.lib.Base);
});
it( 'object inheritance' , () => {
expect(data.obj.__proto__.__proto__).toBe(C.lib.Base.prototype);
});
WordArray和按位运算
按位运算是密码算法的基础。
无论数据类型是什么,按位运算都将它们视为连续序列。 为了降低性能,按位运算最好对连续内存的一部分起作用。 某些语言提供了操作连续内存的方法,例如C ++中的指针和ECMAScript 6中的ArrayBuffer。
JavaScript最初是作为浏览器的脚本语言设计的,因此以前没有进行内存操作。 但是仍然有一种方法来获取按位序列的抽象,即二进制按位运算符。
根据规范,在使用二进制按位运算符进行运算时,所有操作数(无论其原始类型是什么)都将被ToInt32()转换为32位不带int的int。 然后将它们视为32位长度的序列,其结果是32位int不变。 因此,通过将这32位未填充的整数进行拼接,我们可以在连续内存上模拟操作。
基于此,CryptoJS实现了一个名为WordArray的类,以作为按位序列的抽象。 WordArray是CryptoJS最重要的基本类,它的所有算法都在底层实现中使用WordArray对象处理。 因此,了解WordArray是了解其算法的前提。
WordArray的定义在core.js文件中:
请注意,以下所有代码均为entronad / crypto-es 。
export class WordArray extends Base {
constructor (words = [], sigBytes = words.length * 4) {
super ();
this .words = words;
this .sigBytes = sigBytes;
}
...
}
WordArray直接从Base继承。 它具有两个字段:word和sigBytes。 word是一个由32位无感整数组成的数组,通过按顺序拼接此数组的元素,我们可以获得所需的按位序列。 在JavaScript中,32位unsighed int和该位之间的转换是通过Binary Complement实现的。 但是我们不必参与其中,因为此int的值没有意义。 通常,按位序列以字节为单位或以十六进制数表示,因此我们只需要知道32位等于4个字节或8个十六进制数即可。
编码算法的主题是字符串。 因此,按位序列是整个字节的全部或8位的时间。 但是它们不一定是32位的时间。 因此,仅靠单词的长度,我们无法获得按位序列的实际长度,因为可能会有一些空的尾部字节。 因此我们需要字段sigBytes指示实际的有效字节长度。
我们可以通过直接传递以下两个字段来创建WordArray:
const wordArray = CryptoES.lib.WordArray.create([ 0x00010203 , 0x04050607 ], 6 );
为了方便用sigBytes修剪单词,WordArray中提供了一个钳位方法:
clamp() {
// Shortcuts
const { words, sigBytes } = this ;
// Clamp
words[sigBytes >>> 2 ] &= 0xffffffff << ( 32 - (sigBytes % 4 ) * 8 );
words.length = Math .ceil(sigBytes / 4 );
}
它将删除单词中的“无关紧要的字节”。 在word数组中,将保留充满有效字节的起始元素,而没有有效字节的尾部元素将通过word.length = Math.ceil(sigBytes / 4)忽略。
具有有效字节和无关紧要字节的中间元素很难处理。 首先,我们应计算要删除的长度: (32-(sigBytes%4)* 8) ,然后按此长度向左移动0xffffffff以获得32位掩码,然后按sigBytes >>> 2定位此中间元素(与int除以4)相同,最后将其与掩码将无关紧要的字节设置为0。
通过>>>定位元素并使用mask进行定位在CryptoJS中被广泛使用。
就像钳一样,concat的麻烦部分还在于处理中间元素:
concat(wordArray) {
// Shortcuts
const thisWords = this .words;
const thatWords = wordArray.words;
const thisSigBytes = this .sigBytes;
const thatSigBytes = wordArray.sigBytes;
// Clamp excess bits
this .clamp();
// Concat
if (thisSigBytes % 4 ) {
// Copy one byte at a time
for ( let i = 0 ; i < thatSigBytes; i += 1 ) {
const thatByte = (thatWords[i >>> 2 ] >>> ( 24 - (i % 4 ) * 8 )) & 0xff ;
thisWords[(thisSigBytes + i) >>> 2 ] |= thatByte << ( 24 - ((thisSigBytes + i) % 4 ) * 8 );
}
} else {
// Copy one word at a time
for ( let i = 0 ; i < thatSigBytes; i += 4 ) {
thisWords[(thisSigBytes + i) >>> 2 ] = thatWords[i >>> 2 ];
}
}
this .sigBytes += thatSigBytes;
// Chainable
return this ;
}
在CryptoJs内部,WordArray是大多数函数的输入和输出,但是外部用户最关心字符串结果。 因此,WordArray提供了重写的toString方法:
toString(encoder = Hex) {
return encoder.stringify( this );
}
因为单词array是引用类型,所以我们应该在clone方法中按切片创建它的新副本:
clone() {
const clone = super .clone.call( this );
clone._data = this ._data.clone();
return clone;
}
除构造函数外,static方法random提供一定长度的随机WordArray。 由于JavaScript的Math.random()不安全并且返回64位浮点数,因此我们将做一些额外的处理:
static random(nBytes) {
const words = [];
const r = ( m_w ) => {
let _m_w = m_w;
let _m_z = 0x3ade68b1 ;
const mask = 0xffffffff ;
return () => {
_m_z = ( 0x9069 * (_m_z & 0xFFFF ) + (_m_z >> 0x10 )) & mask;
_m_w = ( 0x4650 * (_m_w & 0xFFFF ) + (_m_w >> 0x10 )) & mask;
let result = ((_m_z << 0x10 ) + _m_w) & mask;
result /= 0x100000000 ;
result += 0.5 ;
return result * ( Math .random() > 0.5 ? 1 : -1 );
};
};
for ( let i = 0 , rcache; i < nBytes; i += 4 ) {
const _r = r((rcache || Math .random()) * 0x100000000 );
rcache = _r() * 0x3ade67b7 ;
words.push((_r() * 0x100000000 ) | 0 );
}
return new WordArray(words, nBytes);
}
ArrayBuffer和TypedArray
由于ArrayBuffer包含在ECMAScript中,因此已在越来越多的场景中使用它,例如WebSocket,文件对象和画布输出。 在处理这些形式的对象时,在某些情况下,我们还需要对它们进行加密,解密或散列。
因此CryptoJS扩展了WordArray创建器,以允许ArrayBuffer和TypedArray作为输入。 此扩展位于单个lib-typedArrays.js文件中,并进行了大量检查和重构WordArray创建器以确保兼容性。 我们将其集成到原始WordArray构造函数中,并简化了以下检查:
constructor (words = [], sigBytes = words.length * 4) {
super ();
let typedArray = words;
// Convert buffers to uint8
if (typedArray instanceof ArrayBuffer ) {
typedArray = new Uint8Array (typedArray);
}
// Convert other array views to uint8
if (
typedArray instanceof Int8Array
|| typedArray instanceof Uint8ClampedArray
|| typedArray instanceof Int16Array
|| typedArray instanceof Uint16Array
|| typedArray instanceof Int32Array
|| typedArray instanceof Uint32Array
|| typedArray instanceof Float32Array
|| typedArray instanceof Float64Array
) {
typedArray = new Uint8Array (typedArray.buffer, typedArray.byteOffset, typedArray.byteLength);
}
// Handle Uint8Array
if (typedArray instanceof Uint8Array ) {
// Shortcut
const typedArrayByteLength = typedArray.byteLength;
// Extract bytes
const _words = [];
for ( let i = 0 ; i < typedArrayByteLength; i += 1 ) {
_words[i >>> 2 ] |= typedArray[i] << ( 24 - (i % 4 ) * 8 );
}
// Initialize this word array
this .words = _words;
this .sigBytes = typedArrayByteLength;
} else {
// Else call normal init
this .words = words;
this .sigBytes = sigBytes;
}
}
然后,WordArray创建者可以获取ArrayBuffer或TypedArray,以便CryptoES算法可以应用于它们:
然后,WordArray创建者可以获取ArrayBuffer或TypedArray,以便CryptoES算法可以应用于它们:
const words = CryptoES.lib.WordArray.create( new ArrayBuffer ( 8 ));
const rst = CryptoES.AES.encrypt(words, 'Secret Passphrase' )
请注意,ArrayBuffer无法直接传递给算法,您应该首先将其更改为WordArray。
这样,加密文件会更容易
const fileInput = document .getElementById( 'fileInput' );
const file = fileInput.files[ 0 ];
const reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = function ( ) {
const arrayBuffer = reader.result;
const words = CryptoES.lib.WordArray.create(arrayBuffer);
const rst = CryptoES.AES.encrypt(words, 'Secret Passphrase' )
...
};
敬请关注!
先前发布在https://medium.com/front-end-weekly/refactoring-cryptojs-in-modern-ecmascript-1d4e1837c272
From: https://hackernoon.com/refactoring-cryptojs-in-modern-ecmascript-v9uu36rh