ecmascript_在现代ECMAScript中重构CryptoJS

ecmascript

仓库: Entronad / crypto-es

npm: crypto-es

在前端和后端项目中,加密,解密和哈希的需求非常普遍。 处理敏感数据时,在您的代码中可能经常看到名称为MD5Base64AES类的函数。

在所有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-jsCryptoES将主要基于此版本。 它的“基础对象”在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位未填充的int进行拼接,我们可以模拟连续内存上的操作。

基于此,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

翻译自: https://hackernoon.com/refactoring-cryptojs-in-modern-ecmascript-v9uu36rh

ecmascript

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值