diff --git a/.npmignore b/.npmignore index e623fa5..6af1fa1 100644 --- a/.npmignore +++ b/.npmignore @@ -1 +1,2 @@ -.travis.yml \ No newline at end of file +test +.github \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index dc895a5..62585f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gmsm-sm2js", - "version": "0.6.4", + "version": "0.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gmsm-sm2js", - "version": "0.6.4", + "version": "0.7.0", "license": "Apache-2.0", "dependencies": { "jsrsasign": "^11.1.0" diff --git a/package.json b/package.json index d14f66e..0dfa577 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,17 @@ { "name": "gmsm-sm2js", - "version": "0.6.7", + "version": "0.7.0", "description": "Pure Javascript implementation of the SM2/SM3/SM4 functions based on jsrsasign", "keywords": [ "sm2", "sm3", "sm4", - "gmsm" + "gmsm", + "jsrsasign" ], "main": "index.js", "scripts": { - "test": "standard && node src/cryptojs_sm3_test.js && node src/cryptojs_sm4_test.js && node src/sm2_test.js" + "test": "standard && node test/util_test.js && node test/cryptojs_sm3_test.js && node test/cryptojs_sm4_test.js && node test/sm2_test.js" }, "dependencies": { "jsrsasign": "^11.1.0" diff --git a/src/jsrsasign_patch.js b/src/jsrsasign_patch.js index 618832a..247d376 100644 --- a/src/jsrsasign_patch.js +++ b/src/jsrsasign_patch.js @@ -2,6 +2,7 @@ const rs = require('jsrsasign') const KJUR = rs.KJUR const C = rs.CryptoJS const CEnc = C.enc +const util = require('./util') function parsePBES2 (hP8Prv) { const pASN = rs.ASN1HEX.parse(hP8Prv) @@ -148,7 +149,7 @@ function patchSM4 () { default: throw new Error('unsupported algorithm: ' + algName) } - const cipher = crypto.createCipheriv(cipherMode, Buffer.from(hKey, 'hex'), Buffer.from(param.iv, 'hex')) + const cipher = crypto.createCipheriv(cipherMode, util.hexToUint8Array(hKey), util.hexToUint8Array(param.iv)) return cipher.update(hPlain, 'hex', 'hex') + cipher.final('hex') } const wKey = C.enc.Hex.parse(hKey) @@ -222,7 +223,7 @@ function patchSM4 () { default: throw new Error('unsupported algorithm: ' + algName) } - const cipher = crypto.createDecipheriv(cipherMode, Buffer.from(hKey, 'hex'), Buffer.from(param.iv, 'hex')) + const cipher = crypto.createDecipheriv(cipherMode, util.hexToUint8Array(hKey), util.hexToUint8Array(param.iv)) return cipher.update(hEnc, 'hex', 'hex') + cipher.final('hex') } const wKey = C.enc.Hex.parse(hKey) diff --git a/src/sm2.js b/src/sm2.js index af7c73b..ae147a4 100644 --- a/src/sm2.js +++ b/src/sm2.js @@ -90,7 +90,7 @@ function adaptSM2 (ecdsa) { ecdsa[sm2] = true /** * Encrypt data with SM2 alg - * @param {String|Uint8Array|Buffer} data The data to be encrypted + * @param {string|Uint8Array} data The data to be encrypted * @param {EncrypterOptions} opts options for ciphertext format, default is C1C3C2 * @returns hex string of ciphertext */ @@ -101,12 +101,12 @@ function adaptSM2 (ecdsa) { /** * Encrypt hex data with SM2 alg - * @param {String} data The hex data to be encrypted + * @param {string} data The hex data to be encrypted * @param {EncrypterOptions} opts options for ciphertext format, default is C1C3C2 * @returns hex string of ciphertext */ ecdsa.encryptHex = function (dataHex, opts = DEFAULT_SM2_ENCRYPT_OPTIONS) { - return this.encrypt(new Uint8Array(Buffer.from(dataHex, 'hex')), opts) + return this.encrypt(util.hexToUint8Array(dataHex), opts) } /** @@ -133,7 +133,7 @@ function adaptSM2 (ecdsa) { const k = this.getBigRandom(n) const c1 = G.multiply(k) const s = Q.multiply(k) - const c2 = kdf(new Uint8Array(util.integerToBytes(s.getX().toBigInteger(), SM2_BYTE_SIZE).concat(util.integerToBytes(s.getY().toBigInteger(), SM2_BYTE_SIZE))), dataLen) + const c2 = kdf(Uint8Array.from(util.integerToBytes(s.getX().toBigInteger(), SM2_BYTE_SIZE).concat(util.integerToBytes(s.getY().toBigInteger(), SM2_BYTE_SIZE))), dataLen) if (!c2) { if (count++ > MAX_RETRY) { throw new Error('sm2: A5, failed to calculate valid t') @@ -143,17 +143,17 @@ function adaptSM2 (ecdsa) { for (let i = 0; i < dataLen; i++) { c2[i] ^= data[i] } - md.update(new Uint8Array(util.integerToBytes(s.getX().toBigInteger(), SM2_BYTE_SIZE))) + md.update(Uint8Array.from(util.integerToBytes(s.getX().toBigInteger(), SM2_BYTE_SIZE))) md.update(data) - md.update(new Uint8Array(util.integerToBytes(s.getY().toBigInteger(), SM2_BYTE_SIZE))) + md.update(Uint8Array.from(util.integerToBytes(s.getY().toBigInteger(), SM2_BYTE_SIZE))) const c3 = md.digestRaw() if (opts.getEncodingFormat() === CIPHERTEXT_ENCODING_PLAIN) { - return Buffer.from(c1.getEncoded(false)).toString('hex') + Buffer.from(c3).toString('hex') + Buffer.from(c2).toString('hex') + return util.toHex(c1.getEncoded(false)) + util.toHex(c3) + util.toHex(c2) } const derX = new rs.asn1.DERInteger({ bigint: c1.getX().toBigInteger() }) const derY = new rs.asn1.DERInteger({ bigint: c1.getY().toBigInteger() }) - const derC3 = new rs.asn1.DEROctetString({ hex: Buffer.from(c3).toString('hex') }) - const derC2 = new rs.asn1.DEROctetString({ hex: Buffer.from(c2).toString('hex') }) + const derC3 = new rs.asn1.DEROctetString({ hex: util.toHex(c3) }) + const derC2 = new rs.asn1.DEROctetString({ hex: util.toHex(c2) }) const derSeq = new rs.asn1.DERSequence({ array: [derX, derY, derC3, derC2] }) return derSeq.tohex() } while (true) @@ -161,7 +161,7 @@ function adaptSM2 (ecdsa) { /** * SM2 decryption - * @param {String|Uint8Array|Buffer} data The data to be decrypted + * @param {String|Uint8Array} data The data to be decrypted * @return {String} decrypted hex content */ ecdsa.decrypt = function (data) { @@ -171,18 +171,18 @@ function adaptSM2 (ecdsa) { /** * SM2 decryption - * @param {String} dataHex The hex data to be decrypted - * @return {String} decrypted hex content + * @param {string} dataHex The hex data to be decrypted + * @return {string} decrypted hex content */ ecdsa.decryptHex = function (dataHex) { - return this.decrypt(new Uint8Array(Buffer.from(dataHex, 'hex'))) + return this.decrypt(util.hexToUint8Array(dataHex)) } /** * SM2 decryption (internal function) * @param {Uint8Array} data The hex data to be decrypted * @param {BigInteger} d The SM2 private key - * @return {String} decrypted hex content + * @return {string} decrypted hex content */ ecdsa.decryptRaw = function (data, d) { data = util.normalizeInput(data) @@ -198,7 +198,7 @@ function adaptSM2 (ecdsa) { const s = c1.multiply(d) const c2 = data.subarray(97) const c3 = data.subarray(65, 97) - const plaintext = kdf(new Uint8Array(util.integerToBytes(s.getX().toBigInteger(), SM2_BYTE_SIZE).concat(util.integerToBytes(s.getY().toBigInteger(), SM2_BYTE_SIZE))), dataLen - 97) + const plaintext = kdf(Uint8Array.from(util.integerToBytes(s.getX().toBigInteger(), SM2_BYTE_SIZE).concat(util.integerToBytes(s.getY().toBigInteger(), SM2_BYTE_SIZE))), dataLen - 97) if (!plaintext) { throw new Error('sm2: invalid cipher content') } @@ -207,9 +207,9 @@ function adaptSM2 (ecdsa) { } // check c3 const md = new MessageDigest() - md.update(new Uint8Array(util.integerToBytes(s.getX().toBigInteger(), SM2_BYTE_SIZE))) + md.update(Uint8Array.from(util.integerToBytes(s.getX().toBigInteger(), SM2_BYTE_SIZE))) md.update(plaintext) - md.update(new Uint8Array(util.integerToBytes(s.getY().toBigInteger(), SM2_BYTE_SIZE))) + md.update(Uint8Array.from(util.integerToBytes(s.getY().toBigInteger(), SM2_BYTE_SIZE))) const hash = md.digestRaw() let difference = 0 for (let i = 0; i < hash.length; i++) { @@ -219,7 +219,7 @@ function adaptSM2 (ecdsa) { throw new Error('sm2: decryption error') } - return Buffer.from(plaintext).toString('hex') + return util.toHex(plaintext) } /** @@ -301,7 +301,7 @@ function adaptSM2 (ecdsa) { /** * calculateZA ZA = H256(ENTLA || IDA || a || b || xG || yG || xA || yA) - * @param {String|Uint8Array|Buffer} uid The user id, use default if not specified + * @param {string|Uint8Array} uid The user id, use default if not specified * @returns Uint8Array of the result */ ecdsa.calculateZA = function (uid) { @@ -315,9 +315,9 @@ function adaptSM2 (ecdsa) { } const entla = uidLen << 3 // bit length const md = new MessageDigest() - md.update(new Uint8Array([0xff & (entla >>> 8), 0xff & entla])) + md.update(Uint8Array.from([0xff & (entla >>> 8), 0xff & entla])) md.update(uid) - md.update(new Uint8Array(Buffer.from(SM2_CURVE_PARAMS_FOR_ZA, 'hex'))) // a||b||gx||gy + md.update(util.hexToUint8Array(SM2_CURVE_PARAMS_FOR_ZA)) // a||b||gx||gy let Q if (this.pubKeyHex) { Q = rs.ECPointFp.decodeFromHex(this.ecparams.curve, this.pubKeyHex) @@ -325,10 +325,10 @@ function adaptSM2 (ecdsa) { const d = new rs.BigInteger(this.prvKeyHex, 16) const G = this.ecparams.G Q = G.multiply(d) - this.pubKeyHex = Buffer.from(Q.getEncoded()).toString('hex') + this.pubKeyHex = util.toHex(Q.getEncoded()) } - md.update(new Uint8Array(util.integerToBytes(Q.getX().toBigInteger(), SM2_BYTE_SIZE))) // x - md.update(new Uint8Array(util.integerToBytes(Q.getY().toBigInteger(), SM2_BYTE_SIZE))) // y + md.update(Uint8Array.from(util.integerToBytes(Q.getX().toBigInteger(), SM2_BYTE_SIZE))) // x + md.update(Uint8Array.from(util.integerToBytes(Q.getY().toBigInteger(), SM2_BYTE_SIZE))) // y return md.digestRaw() } } @@ -336,8 +336,8 @@ function adaptSM2 (ecdsa) { /** * SM2 KDF function - * @param {String|Uint8Array|Buffer} data The salt for kdf - * @param {Number} len The request key bytes length + * @param {string|Uint8Array} data The salt for kdf + * @param {number} len The request key bytes length * @returns Uint8Array of the generated key */ function kdf (data, len) { @@ -394,7 +394,7 @@ class MessageDigest { */ updateHex (hex) { if (useNodeSM3) { - this.md.update(new Uint8Array(Buffer.from(hex, 'hex'))) + this.md.update(util.hexToUint8Array(hex)) } else { this.md.update(rs.CryptoJS.enc.Hex.parse(hex)) } @@ -618,7 +618,7 @@ class Signature { * SM2 encryption function * * @param {string|object} pubkey hex public key string or ECDSA object - * @param {string|Buffer|Uint8Array} data plaintext data + * @param {string|Uint8Array} data plaintext data * @param {EncrypterOptions} opts options, just support encodingFormat now, default is plain encoding format * @returns hex plain format ciphertext */ @@ -643,7 +643,7 @@ function encrypt (pubkey, data, opts = DEFAULT_SM2_ENCRYPT_OPTIONS) { * @returns hex ans.1 format ciphertext */ function plainCiphertext2ASN1 (data) { - data = new Uint8Array(Buffer.from(data, 'hex')) + data = util.hexToUint8Array(data) const dataLen = data.length if (data[0] !== UNCOMPRESSED) { @@ -657,8 +657,8 @@ function plainCiphertext2ASN1 (data) { const c3 = data.subarray(65, 97) const derX = new rs.asn1.DERInteger({ bigint: point1.getX().toBigInteger() }) const derY = new rs.asn1.DERInteger({ bigint: point1.getY().toBigInteger() }) - const derC3 = new rs.asn1.DEROctetString({ hex: Buffer.from(c3).toString('hex') }) - const derC2 = new rs.asn1.DEROctetString({ hex: Buffer.from(c2).toString('hex') }) + const derC3 = new rs.asn1.DEROctetString({ hex: util.toHex(c3) }) + const derC2 = new rs.asn1.DEROctetString({ hex: util.toHex(c2) }) const derSeq = new rs.asn1.DERSequence({ array: [derX, derY, derC3, derC2] }) return derSeq.getEncodedHex() @@ -704,7 +704,7 @@ function asn1Ciphertext2Plain (hexASN1Data) { const c3 = aValue[2] const c2 = aValue[3] - return Buffer.from(point.getEncoded(false)).toString('hex') + c3 + c2 + return util.toHex(point.getEncoded(false)) + c3 + c2 } /** @@ -716,14 +716,14 @@ function asn1Ciphertext2Plain (hexASN1Data) { * @returns hex plain format ciphertext */ function encryptHex (pubkey, data, opts = DEFAULT_SM2_ENCRYPT_OPTIONS) { - return encrypt(pubkey, new Uint8Array(Buffer.from(data, 'hex')), opts) + return encrypt(pubkey, util.hexToUint8Array(data), opts) } /** * SM2 decrypt function * * @param {string|object} prvKey private key used to decrypt, private key hex string or ECDSA object. - * @param {string|Buffer|Uint8Array} data plain format (C1||C3|C2) ciphertext data + * @param {string|Uint8Array} data plain format (C1||C3|C2) ciphertext data * @returns hex plaintext */ function decrypt (prvKey, data) { @@ -758,7 +758,7 @@ function decryptHex (prvKey, data) { if (tag === '30') { data = asn1Ciphertext2Plain(data) } - return decrypt(prvKey, new Uint8Array(Buffer.from(data, 'hex'))) + return decrypt(prvKey, util.hexToUint8Array(data)) } function getCurveName () { @@ -780,7 +780,7 @@ rs.asn1.csr.CSRUtil.newCSRPEM = function (param) { const hCSRI = (new rs.asn1.csr.CertificationRequestInfo(this.params)).getEncodedHex() const sig = new Signature({ alg: this.params.sigalg }) sig.init(this.params.sbjprvkey) - const sighex = sig.sm2Sign(new Uint8Array(Buffer.from(hCSRI, 'hex'))) + const sighex = sig.sm2Sign(util.hexToUint8Array(hCSRI)) this.params.sighex = sighex } } @@ -801,7 +801,7 @@ function createX509 () { const sig = new Signature({ alg: algName }) sig.init(pubKey) - return sig.sm2Verify(hSigVal, new Uint8Array(Buffer.from(hTbsCert, 'hex'))) + return sig.sm2Verify(hSigVal, util.hexToUint8Array(hTbsCert)) } return x } diff --git a/src/util.js b/src/util.js index 3c2b4b2..fc2dee5 100644 --- a/src/util.js +++ b/src/util.js @@ -11,22 +11,79 @@ function integerToBytes (i, len) { return bytes } -// For convenience, let people hash a string, not just a Uint8Array -function normalizeInput (input) { - let ret +/** + * Convert byte array or Uint8Array to hex string + * @param {Uint8Array|Array} bytes byte array or Uint8Array + * @returns {string} hex string + */ +function toHex (bytes) { + const isUint8Array = bytes instanceof Uint8Array + if (!isUint8Array) { + bytes = Uint8Array.from(bytes) + } + return Array.prototype.map + .call(bytes, function (n) { + return (n < 16 ? '0' : '') + n.toString(16) + }) + .join('') +} + +/** + * Convert a hex string to a Uint8Array. + * @param {string} hexStr - Hex string to convert + * @return {Uint8Array} Uint8Array containing the converted hex string. + */ +function hexToUint8Array (hexStr) { + if (typeof hexStr !== 'string' || hexStr.length % 2 === 1) { + throw new Error('Invalid hex string') + } + const bytes = [] + for (let i = 0; i < hexStr.length; i += 2) { + bytes.push(parseInt(hexStr.substring(i, i + 2), 16)) + } + return Uint8Array.from(bytes) +} + +const hasBuffer = typeof Buffer !== 'undefined' +const hasTextEncoder = typeof TextEncoder !== 'undefined' + +function _normalizeInputWithBuffer (input) { if (input instanceof Uint8Array) { - ret = input - } else if (input instanceof Buffer) { - ret = new Uint8Array(input) - } else if (typeof input === 'string') { - ret = new Uint8Array(Buffer.from(input, 'utf8')) - } else { - throw new Error('Input must be an string, Buffer or Uint8Array') + return input + } + if (input instanceof Buffer) { + return Uint8Array.from(input) } - return ret + if (typeof input === 'string') { + return hasTextEncoder ? new TextEncoder().encode(input) : Uint8Array.from(Buffer.from(input, 'utf8')) + } + throw new Error('Input must be an utf8 string, Buffer or Uint8Array') +} + +function _normalizeInputWithoutBuffer (input) { + if (input instanceof Uint8Array) { + return input + } + + if (typeof input === 'string') { + if (hasTextEncoder) { + return new TextEncoder().encode(input) + } + input = unescape(encodeURIComponent(input)) + const array = new Uint8Array(input.length) + for (let i = 0; i < input.length; i++) { + array[i] = input.charCodeAt(i) + } + return Uint8Array.from(array) + } + throw new Error('Input must be an utf8 string or Uint8Array') } +const normalizeInput = hasBuffer ? _normalizeInputWithBuffer : _normalizeInputWithoutBuffer + module.exports = { integerToBytes, - normalizeInput + normalizeInput, + hexToUint8Array, + toHex } diff --git a/src/cryptojs_sm3_test.js b/test/cryptojs_sm3_test.js similarity index 96% rename from src/cryptojs_sm3_test.js rename to test/cryptojs_sm3_test.js index bfcba70..958bf91 100644 --- a/src/cryptojs_sm3_test.js +++ b/test/cryptojs_sm3_test.js @@ -1,4 +1,4 @@ -require('./cryptojs_sm3') +require('../src/cryptojs_sm3') const test = require('tape') const rs = require('jsrsasign') diff --git a/src/cryptojs_sm4_test.js b/test/cryptojs_sm4_test.js similarity index 96% rename from src/cryptojs_sm4_test.js rename to test/cryptojs_sm4_test.js index a16ddb8..305e22c 100644 --- a/src/cryptojs_sm4_test.js +++ b/test/cryptojs_sm4_test.js @@ -1,4 +1,4 @@ -require('./cryptojs_sm4') +require('../src/cryptojs_sm4') const test = require('tape') const sjcl = require('sjcl-with-all') const rs = require('jsrsasign') diff --git a/src/jsrsasign_patch_test.js b/test/jsrsasign_patch_test.js similarity index 93% rename from src/jsrsasign_patch_test.js rename to test/jsrsasign_patch_test.js index be60052..3e764b5 100644 --- a/src/jsrsasign_patch_test.js +++ b/test/jsrsasign_patch_test.js @@ -1,7 +1,7 @@ const test = require('tape') const rs = require('jsrsasign') const KJUR = rs.KJUR -require('./jsrsasign_patch').patch() +require('../src/jsrsasign_patch').patch() test('test sm4-cbc with KJUR.crypto.Cipher', (t) => { const cases = [ diff --git a/src/sm2_test.js b/test/sm2_test.js similarity index 96% rename from src/sm2_test.js rename to test/sm2_test.js index 624b668..a97f1b3 100644 --- a/src/sm2_test.js +++ b/test/sm2_test.js @@ -1,6 +1,7 @@ const test = require('tape') const rs = require('jsrsasign') -const sm2 = require('./sm2') +const sm2 = require('../src/sm2') +const util = require('../src/util') const publicKeyPemFromAliKmsForSign = `-----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAERrsLH25zLm2LIo6tivZM9afLprSX @@ -171,7 +172,7 @@ test('SM2 parse public key pem, verify signature, both from ali KMS', function ( test('SM2 calculate ZA', function (t) { const sig = sm2.createSM2Signature() sig.init(publicKeyPemFromAliKmsForSign) - const za = Buffer.from(sig.pubKey.calculateZA()).toString('hex') + const za = util.toHex(sig.pubKey.calculateZA()) t.equal(za, '17e7fc071f1418200aeead3c5118a2f18381431d92b808a3bd1ba2d8270c2914') t.end() }) diff --git a/test/util_test.js b/test/util_test.js new file mode 100644 index 0000000..489755a --- /dev/null +++ b/test/util_test.js @@ -0,0 +1,9 @@ +const test = require('tape') +const util = require('../src/util') + +test('UTF8 string to Uint8Array test', function (t) { + const chinese = '你好世界' + const result = util.toHex(util.normalizeInput(chinese)) + t.equals(result, 'e4bda0e5a5bde4b896e7958c') + t.end() +})