From 6155634d3b9e8c5044a5415540c073211a0df642 Mon Sep 17 00:00:00 2001 From: swesven <81263441+swesven@users.noreply.github.com> Date: Wed, 24 Mar 2021 00:58:54 +0100 Subject: [PATCH] Add the SM4 block cipher, also a no-padding option for block ciphers. This adds an implementation of the SM4 block cipher, and operations to encrypt and decrypt using it with CBC,ECB,CFB,OFB,CTR modes. Also, a "no padding" option is added for AES,DES,3DES and SM4 decryption in ECB/CBC modes. This variant does not attempt to validate the last block as being PKCS#7 padded. This is useful, both since other padding schemes exist, and also for decrypting data where the final block is missing. --- src/core/config/Categories.json | 2 + src/core/lib/SM4.mjs | 329 +++++++++++++++++++++++ src/core/operations/AESDecrypt.mjs | 18 +- src/core/operations/DESDecrypt.mjs | 13 +- src/core/operations/SM4Decrypt.mjs | 88 ++++++ src/core/operations/SM4Encrypt.mjs | 88 ++++++ src/core/operations/TripleDESDecrypt.mjs | 10 +- tests/operations/index.mjs | 1 + tests/operations/tests/SM4.mjs | 279 +++++++++++++++++++ 9 files changed, 821 insertions(+), 7 deletions(-) create mode 100644 src/core/lib/SM4.mjs create mode 100644 src/core/operations/SM4Decrypt.mjs create mode 100644 src/core/operations/SM4Encrypt.mjs create mode 100644 tests/operations/tests/SM4.mjs diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 3a5eb0d5..069063b0 100755 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -83,6 +83,8 @@ "ROT47", "XOR", "XOR Brute Force", + "SM4 Encrypt", + "SM4 Decrypt", "Vigenère Encode", "Vigenère Decode", "To Morse Code", diff --git a/src/core/lib/SM4.mjs b/src/core/lib/SM4.mjs new file mode 100644 index 00000000..36648348 --- /dev/null +++ b/src/core/lib/SM4.mjs @@ -0,0 +1,329 @@ +/** + * Complete implementation of SM4 cipher encryption/decryption with + * ECB, CBC, CFB, OFB, CTR block modes. + * These modes are specified in IETF draft-ribose-cfrg-sm4-09, see: + * https://tools.ietf.org/id/draft-ribose-cfrg-sm4-09.html + * for details. + * + * Follows spec from Cryptography Standardization Technical Comittee: + * http://www.gmbz.org.cn/upload/2018-04-04/1522788048733065051.pdf + * + * @author swesven + * @copyright 2021 + * @license Apache-2.0 + */ + +import OperationError from "../errors/OperationError.mjs"; + +/** Number of rounds */ +const NROUNDS = 32; + +/** block size in bytes */ +const BLOCKSIZE = 16; + +/** The S box, 256 8-bit values */ +const Sbox = [ + 0xd6, 0x90, 0xe9, 0xfe, 0xcc, 0xe1, 0x3d, 0xb7, 0x16, 0xb6, 0x14, 0xc2, 0x28, 0xfb, 0x2c, 0x05, + 0x2b, 0x67, 0x9a, 0x76, 0x2a, 0xbe, 0x04, 0xc3, 0xaa, 0x44, 0x13, 0x26, 0x49, 0x86, 0x06, 0x99, + 0x9c, 0x42, 0x50, 0xf4, 0x91, 0xef, 0x98, 0x7a, 0x33, 0x54, 0x0b, 0x43, 0xed, 0xcf, 0xac, 0x62, + 0xe4, 0xb3, 0x1c, 0xa9, 0xc9, 0x08, 0xe8, 0x95, 0x80, 0xdf, 0x94, 0xfa, 0x75, 0x8f, 0x3f, 0xa6, + 0x47, 0x07, 0xa7, 0xfc, 0xf3, 0x73, 0x17, 0xba, 0x83, 0x59, 0x3c, 0x19, 0xe6, 0x85, 0x4f, 0xa8, + 0x68, 0x6b, 0x81, 0xb2, 0x71, 0x64, 0xda, 0x8b, 0xf8, 0xeb, 0x0f, 0x4b, 0x70, 0x56, 0x9d, 0x35, + 0x1e, 0x24, 0x0e, 0x5e, 0x63, 0x58, 0xd1, 0xa2, 0x25, 0x22, 0x7c, 0x3b, 0x01, 0x21, 0x78, 0x87, + 0xd4, 0x00, 0x46, 0x57, 0x9f, 0xd3, 0x27, 0x52, 0x4c, 0x36, 0x02, 0xe7, 0xa0, 0xc4, 0xc8, 0x9e, + 0xea, 0xbf, 0x8a, 0xd2, 0x40, 0xc7, 0x38, 0xb5, 0xa3, 0xf7, 0xf2, 0xce, 0xf9, 0x61, 0x15, 0xa1, + 0xe0, 0xae, 0x5d, 0xa4, 0x9b, 0x34, 0x1a, 0x55, 0xad, 0x93, 0x32, 0x30, 0xf5, 0x8c, 0xb1, 0xe3, + 0x1d, 0xf6, 0xe2, 0x2e, 0x82, 0x66, 0xca, 0x60, 0xc0, 0x29, 0x23, 0xab, 0x0d, 0x53, 0x4e, 0x6f, + 0xd5, 0xdb, 0x37, 0x45, 0xde, 0xfd, 0x8e, 0x2f, 0x03, 0xff, 0x6a, 0x72, 0x6d, 0x6c, 0x5b, 0x51, + 0x8d, 0x1b, 0xaf, 0x92, 0xbb, 0xdd, 0xbc, 0x7f, 0x11, 0xd9, 0x5c, 0x41, 0x1f, 0x10, 0x5a, 0xd8, + 0x0a, 0xc1, 0x31, 0x88, 0xa5, 0xcd, 0x7b, 0xbd, 0x2d, 0x74, 0xd0, 0x12, 0xb8, 0xe5, 0xb4, 0xb0, + 0x89, 0x69, 0x97, 0x4a, 0x0c, 0x96, 0x77, 0x7e, 0x65, 0xb9, 0xf1, 0x09, 0xc5, 0x6e, 0xc6, 0x84, + 0x18, 0xf0, 0x7d, 0xec, 0x3a, 0xdc, 0x4d, 0x20, 0x79, 0xee, 0x5f, 0x3e, 0xd7, 0xcb, 0x39, 0x48]; + +/** "Fixed parameter CK" used in key expansion */ +const CK = [ + 0x00070e15, 0x1c232a31, 0x383f464d, 0x545b6269, + 0x70777e85, 0x8c939aa1, 0xa8afb6bd, 0xc4cbd2d9, + 0xe0e7eef5, 0xfc030a11, 0x181f262d, 0x343b4249, + 0x50575e65, 0x6c737a81, 0x888f969d, 0xa4abb2b9, + 0xc0c7ced5, 0xdce3eaf1, 0xf8ff060d, 0x141b2229, + 0x30373e45, 0x4c535a61, 0x686f767d, 0x848b9299, + 0xa0a7aeb5, 0xbcc3cad1, 0xd8dfe6ed, 0xf4fb0209, + 0x10171e25, 0x2c333a41, 0x484f565d, 0x646b7279 +]; + +/** "System parameter FK" */ +const FK = [0xa3b1bac6, 0x56aa3350, 0x677d9197, 0xb27022dc]; + +/** + * Rotating 32-bit shift left + * + * (Note that although JS integers are stored in doubles and thus have 53 bits, + * the JS bitwise operations are 32-bit) + */ +function ROL(i, n) { + return (i << n) | (i >>> (32 - n)); +} + +/** + * Linear transformation L + * + * @param {integer} b - a 32 bit integer + */ +function transformL(b) { + /* Replace each of the 4 bytes in b with the value at its offset in the Sbox */ + b = (Sbox[(b >>> 24) & 0xFF] << 24) | (Sbox[(b >>> 16) & 0xFF] << 16) | + (Sbox[(b >>> 8) & 0xFF] << 8) | Sbox[b & 0xFF]; + /* circular rotate and xor */ + return b ^ ROL(b, 2) ^ ROL(b, 10) ^ ROL(b, 18) ^ ROL(b, 24); +} + +/** + * Linear transformation L' + * + * @param {integer} b - a 32 bit integer + */ +function transformLprime(b) { + /* Replace each of the 4 bytes in b with the value at its offset in the Sbox */ + b = (Sbox[(b >>> 24) & 0xFF] << 24) | (Sbox[(b >>> 16) & 0xFF] << 16) | + (Sbox[(b >>> 8) & 0xFF] << 8) | Sbox[b & 0xFF]; + return b ^ ROL(b, 13) ^ ROL(b, 23); /* circular rotate and XOR */ +} + +/** + * Initialize the round key + */ +function initSM4RoundKey(rawkey) { + const K = rawkey.map((a, i) => a ^ FK[i]); /* K = rawkey ^ FK */ + const roundKey = []; + for (let i = 0; i < 32; i++) + roundKey[i] = K[i + 4] = K[i] ^ transformLprime(K[i + 1] ^ K[i + 2] ^ K[i + 3] ^ CK[i]); + return roundKey; +} + +/** + * Encrypts/decrypts a single block X (4 32-bit values) with a prepared round key. + * + * @param {intArray} X - A cleartext block. + * @param {intArray} roundKey - The round key from initSMRoundKey for encrypting (reversed for decrypting). + * @returns {byteArray} - The cipher text. + */ +function encryptBlockSM4(X, roundKey) { + for (let i = 0; i < NROUNDS; i++) + X[i + 4] = X[i] ^ transformL(X[i + 1] ^ X[i + 2] ^ X[i + 3] ^ roundKey[i]); + return [X[35], X[34], X[33], X[32]]; +} + +/** + * Takes 16 bytes from an offset in an array and returns an array of 4 32-bit Big-Endian values. + * (DataView won't work portably here as we need Big-Endian) + * + * @param {byteArray} bArray - the array of bytes + * @param {integer} offset - starting offset in the array; 15 bytes must follow it. + */ +function bytesToInts(bArray, offs=0) { + let offset = offs; + const A = (bArray[offset] << 24) | (bArray[offset + 1] << 16) | (bArray[offset + 2] << 8) | bArray[offset + 3]; + offset += 4; + const B = (bArray[offset] << 24) | (bArray[offset + 1] << 16) | (bArray[offset + 2] << 8) | bArray[offset + 3]; + offset += 4; + const C = (bArray[offset] << 24) | (bArray[offset + 1] << 16) | (bArray[offset + 2] << 8) | bArray[offset + 3]; + offset += 4; + const D = (bArray[offset] << 24) | (bArray[offset + 1] << 16) | (bArray[offset + 2] << 8) | bArray[offset + 3]; + return [A, B, C, D]; +} + +/** + * Inverse of bytesToInts above; takes an array of 32-bit integers and turns it into an array of bytes. + * Again, Big-Endian order. + */ +function intsToBytes(ints) { + const bArr = []; + for (let i = 0; i < ints.length; i++) { + bArr.push((ints[i] >> 24) & 0xFF); + bArr.push((ints[i] >> 16) & 0xFF); + bArr.push((ints[i] >> 8) & 0xFF); + bArr.push(ints[i] & 0xFF); + } + return bArr; +} + +/** + * Encrypt using SM4 using a given block cipher mode. + * + * @param {byteArray} message - The clear text message; any length under 32 Gb or so. + * @param {byteArray} key - The cipher key, 16 bytes. + * @param {byteArray} iv - The IV or nonce, 16 bytes (not used with ECB mode) + * @param {string} mode - The block cipher mode "CBC", "ECB", "CFB", "OFB", "CTR". + * @param {boolean} noPadding - Don't add PKCS#7 padding if set. + * @returns {byteArray} - The cipher text. + */ +export function encryptSM4(message, key, iv, mode="ECB", noPadding=false) { + const messageLength = message.length; + if (messageLength === 0) + return []; + const roundKey = initSM4RoundKey(bytesToInts(key, 0)); + + /* Pad with PKCS#7 if requested for ECB/CBC else add zeroes (which are sliced off at the end) */ + let padByte = 0; + let nPadding = 16 - (message.length & 0xF); + if (mode === "ECB" || mode === "CBC") { + if (noPadding) { + if (nPadding !== 16) + throw new OperationError("No padding requested in "+mode+" mode but input is not a 16-byte multiple."); + nPadding = 0; + } else + padByte = nPadding; + } + for (let i = 0; i < nPadding; i++) + message.push(padByte); + + const cipherText = []; + switch (mode) { + case "ECB": + for (let i = 0; i < message.length; i += BLOCKSIZE) + Array.prototype.push.apply(cipherText, intsToBytes(encryptBlockSM4(bytesToInts(message, i), roundKey))); + break; + case "CBC": + iv = bytesToInts(iv, 0); + for (let i = 0; i < message.length; i += BLOCKSIZE) { + const block = bytesToInts(message, i); + block[0] ^= iv[0]; block[1] ^= iv[1]; + block[2] ^= iv[2]; block[3] ^= iv[3]; + iv = encryptBlockSM4(block, roundKey); + Array.prototype.push.apply(cipherText, intsToBytes(iv)); + } + break; + case "CFB": + iv = bytesToInts(iv, 0); + for (let i = 0; i < message.length; i += BLOCKSIZE) { + iv = encryptBlockSM4(iv, roundKey); + const block = bytesToInts(message, i); + block[0] ^= iv[0]; block[1] ^= iv[1]; + block[2] ^= iv[2]; block[3] ^= iv[3]; + Array.prototype.push.apply(cipherText, intsToBytes(block)); + iv = block; + } + break; + case "OFB": + iv = bytesToInts(iv, 0); + for (let i = 0; i < message.length; i += BLOCKSIZE) { + iv = encryptBlockSM4(iv, roundKey); + const block = bytesToInts(message, i); + block[0] ^= iv[0]; block[1] ^= iv[1]; + block[2] ^= iv[2]; block[3] ^= iv[3]; + Array.prototype.push.apply(cipherText, intsToBytes(block)); + } + break; + case "CTR": + iv = bytesToInts(iv, 0); + for (let i = 0; i < message.length; i += BLOCKSIZE) { + let iv2 = [...iv]; /* containing the IV + counter */ + iv2[3] += (i >> 4);/* Using a 32 bit counter here. 64 Gb encrypts should be enough for everyone. */ + iv2 = encryptBlockSM4(iv2, roundKey); + const block = bytesToInts(message, i); + block[0] ^= iv2[0]; block[1] ^= iv2[1]; + block[2] ^= iv2[2]; block[3] ^= iv2[3]; + Array.prototype.push.apply(cipherText, intsToBytes(block)); + } + break; + default: + throw new OperationError("Invalid block cipher mode: "+mode); + } + if (mode !== "ECB" && mode !== "CBC") + return cipherText.slice(0, messageLength); + return cipherText; +} + +/** + * Decrypt using SM4 using a given block cipher mode. + * + * @param {byteArray} cipherText - The ciphertext + * @param {byteArray} key - The cipher key, 16 bytes. + * @param {byteArray} iv - The IV or nonce, 16 bytes (not used with ECB mode) + * @param {string} mode - The block cipher mode "CBC", "ECB", "CFB", "OFB", "CTR" + * @param {boolean] ignorePadding - If true, ignore padding issues in ECB/CBC mode. + * @returns {byteArray} - The cipher text. + */ +export function decryptSM4(cipherText, key, iv, mode="ECB", ignorePadding=false) { + const originalLength = cipherText.length; + if (originalLength === 0) + return []; + let roundKey = initSM4RoundKey(bytesToInts(key, 0)); + + if (mode === "ECB" || mode === "CBC") { + /* Init decryption key */ + roundKey = roundKey.reverse(); + if ((originalLength & 0xF) !== 0 && !ignorePadding) + throw new OperationError("With ECB or CBC modes, the input must be divisible into 16 byte blocks. ("+(cipherText.length & 0xF)+" bytes extra)"); + } else /* Pad dummy bytes for other modes, chop them off at the end */ + while ((cipherText.length & 0xF) !== 0) + cipherText.push(0); + + const clearText = []; + switch (mode) { + case "ECB": + for (let i = 0; i < cipherText.length; i += BLOCKSIZE) + Array.prototype.push.apply(clearText, intsToBytes(encryptBlockSM4(bytesToInts(cipherText, i), roundKey))); + break; + case "CBC": + iv = bytesToInts(iv, 0); + for (let i = 0; i < cipherText.length; i += BLOCKSIZE) { + const block = encryptBlockSM4(bytesToInts(cipherText, i), roundKey); + block[0] ^= iv[0]; block[1] ^= iv[1]; + block[2] ^= iv[2]; block[3] ^= iv[3]; + Array.prototype.push.apply(clearText, intsToBytes(block)); + iv = bytesToInts(cipherText, i); + } + break; + case "CFB": + iv = bytesToInts(iv, 0); + for (let i = 0; i < cipherText.length; i += BLOCKSIZE) { + iv = encryptBlockSM4(iv, roundKey); + const block = bytesToInts(cipherText, i); + block[0] ^= iv[0]; block[1] ^= iv[1]; + block[2] ^= iv[2]; block[3] ^= iv[3]; + Array.prototype.push.apply(clearText, intsToBytes(block)); + iv = bytesToInts(cipherText, i); + } + break; + case "OFB": + iv = bytesToInts(iv, 0); + for (let i = 0; i < cipherText.length; i += BLOCKSIZE) { + iv = encryptBlockSM4(iv, roundKey); + const block = bytesToInts(cipherText, i); + block[0] ^= iv[0]; block[1] ^= iv[1]; + block[2] ^= iv[2]; block[3] ^= iv[3]; + Array.prototype.push.apply(clearText, intsToBytes(block)); + } + break; + case "CTR": + iv = bytesToInts(iv, 0); + for (let i = 0; i < cipherText.length; i += BLOCKSIZE) { + let iv2 = [...iv]; /* containing the IV + counter */ + iv2[3] += (i >> 4);/* Using a 32 bit counter here. 64 Gb encrypts should be enough for everyone. */ + iv2 = encryptBlockSM4(iv2, roundKey); + const block = bytesToInts(cipherText, i); + block[0] ^= iv2[0]; block[1] ^= iv2[1]; + block[2] ^= iv2[2]; block[3] ^= iv2[3]; + Array.prototype.push.apply(clearText, intsToBytes(block)); + } + break; + default: + throw new OperationError("Invalid block cipher mode: "+mode); + } + /* Check PKCS#7 padding */ + if (mode === "ECB" || mode === "CBC") { + if (ignorePadding) + return clearText; + const padByte = clearText[clearText.length - 1]; + if (padByte > 16) + throw new OperationError("Invalid PKCS#7 padding."); + for (let i = 0; i < padByte; i++) + if (clearText[clearText.length -i - 1] !== padByte) + throw new OperationError("Invalid PKCS#7 padding."); + return clearText.slice(0, clearText.length - padByte); + } + return clearText.slice(0, originalLength); +} + diff --git a/src/core/operations/AESDecrypt.mjs b/src/core/operations/AESDecrypt.mjs index 1276d139..b45fb6d1 100644 --- a/src/core/operations/AESDecrypt.mjs +++ b/src/core/operations/AESDecrypt.mjs @@ -22,7 +22,7 @@ class AESDecrypt extends Operation { this.name = "AES Decrypt"; this.module = "Ciphers"; - this.description = "Advanced Encryption Standard (AES) is a U.S. Federal Information Processing Standard (FIPS). It was selected after a 5-year process where 15 competing designs were evaluated.

Key: The following algorithms will be used based on the size of the key:

IV: The Initialization Vector should be 16 bytes long. If not entered, it will default to 16 null bytes.

Padding: In CBC and ECB mode, PKCS#7 padding will be used.

GCM Tag: This field is ignored unless 'GCM' mode is used."; + this.description = "Advanced Encryption Standard (AES) is a U.S. Federal Information Processing Standard (FIPS). It was selected after a 5-year process where 15 competing designs were evaluated.

Key: The following algorithms will be used based on the size of the key:

IV: The Initialization Vector should be 16 bytes long. If not entered, it will default to 16 null bytes.

Padding: In CBC and ECB mode, PKCS#7 padding will be used as a default.

GCM Tag: This field is ignored unless 'GCM' mode is used."; this.infoURL = "https://wikipedia.org/wiki/Advanced_Encryption_Standard"; this.inputType = "string"; this.outputType = "string"; @@ -66,6 +66,14 @@ class AESDecrypt extends Operation { { name: "ECB", off: [5, 6] + }, + { + name: "CBC/NoPadding", + off: [5, 6] + }, + { + name: "ECB/NoPadding", + off: [5, 6] } ] }, @@ -104,7 +112,7 @@ class AESDecrypt extends Operation { run(input, args) { const key = Utils.convertToByteString(args[0].string, args[0].option), iv = Utils.convertToByteString(args[1].string, args[1].option), - mode = args[2], + mode = args[2].substring(0, 3), inputType = args[3], outputType = args[4], gcmTag = Utils.convertToByteString(args[5].string, args[5].option), @@ -122,6 +130,12 @@ The following algorithms will be used based on the size of the key: input = Utils.convertToByteString(input, inputType); const decipher = forge.cipher.createDecipher("AES-" + mode, key); + /* Allow for a "no padding" mode */ + if (args[2].endsWith("NoPadding")) { + decipher.mode.unpad = function(output, options) { + return true; + }; + } decipher.start({ iv: iv.length === 0 ? "" : iv, tag: mode === "GCM" ? gcmTag : undefined, diff --git a/src/core/operations/DESDecrypt.mjs b/src/core/operations/DESDecrypt.mjs index 14bbe832..13ae7482 100644 --- a/src/core/operations/DESDecrypt.mjs +++ b/src/core/operations/DESDecrypt.mjs @@ -22,7 +22,7 @@ class DESDecrypt extends Operation { this.name = "DES Decrypt"; this.module = "Ciphers"; - this.description = "DES is a previously dominant algorithm for encryption, and was published as an official U.S. Federal Information Processing Standard (FIPS). It is now considered to be insecure due to its small key size.

Key: DES uses a key length of 8 bytes (64 bits).
Triple DES uses a key length of 24 bytes (192 bits).

IV: The Initialization Vector should be 8 bytes long. If not entered, it will default to 8 null bytes.

Padding: In CBC and ECB mode, PKCS#7 padding will be used."; + this.description = "DES is a previously dominant algorithm for encryption, and was published as an official U.S. Federal Information Processing Standard (FIPS). It is now considered to be insecure due to its small key size.

Key: DES uses a key length of 8 bytes (64 bits).
Triple DES uses a key length of 24 bytes (192 bits).

IV: The Initialization Vector should be 8 bytes long. If not entered, it will default to 8 null bytes.

Padding: In CBC and ECB mode, PKCS#7 padding will be used as a default."; this.infoURL = "https://wikipedia.org/wiki/Data_Encryption_Standard"; this.inputType = "string"; this.outputType = "string"; @@ -42,7 +42,7 @@ class DESDecrypt extends Operation { { "name": "Mode", "type": "option", - "value": ["CBC", "CFB", "OFB", "CTR", "ECB"] + "value": ["CBC", "CFB", "OFB", "CTR", "ECB", "CBC/NoPadding", "ECB/NoPadding"] }, { "name": "Input", @@ -65,7 +65,8 @@ class DESDecrypt extends Operation { run(input, args) { const key = Utils.convertToByteString(args[0].string, args[0].option), iv = Utils.convertToByteArray(args[1].string, args[1].option), - [,, mode, inputType, outputType] = args; + mode = args[2].substring(0, 3), + [,,, inputType, outputType] = args; if (key.length !== 8) { throw new OperationError(`Invalid key length: ${key.length} bytes @@ -83,6 +84,12 @@ Make sure you have specified the type correctly (e.g. Hex vs UTF8).`); input = Utils.convertToByteString(input, inputType); const decipher = forge.cipher.createDecipher("DES-" + mode, key); + /* Allow for a "no padding" mode */ + if (args[2].endsWith("NoPadding")) { + decipher.mode.unpad = function(output, options) { + return true; + }; + } decipher.start({iv: iv}); decipher.update(forge.util.createBuffer(input)); const result = decipher.finish(); diff --git a/src/core/operations/SM4Decrypt.mjs b/src/core/operations/SM4Decrypt.mjs new file mode 100644 index 00000000..7ee3e689 --- /dev/null +++ b/src/core/operations/SM4Decrypt.mjs @@ -0,0 +1,88 @@ +/** + * @author swesven + * @copyright 2021 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import Utils from "../Utils.mjs"; +import OperationError from "../errors/OperationError.mjs"; +import { toHex } from "../lib/Hex.mjs"; +import { decryptSM4 } from "../lib/SM4.mjs"; + +/** + * SM4 Decrypt operation + */ +class SM4Decrypt extends Operation { + + /** + * SM4Encrypt constructor + */ + constructor() { + super(); + + this.name = "SM4 Decrypt"; + this.module = "Ciphers"; + this.description = "SM4 is a 128-bit block cipher, currently established as a national standard (GB/T 32907-2016) of China."; + this.infoURL = "https://en.wikipedia.org/wiki/SM4_(cipher)"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + "name": "Key", + "type": "toggleString", + "value": "", + "toggleValues": ["Hex", "UTF8", "Latin1", "Base64"] + }, + { + "name": "IV", + "type": "toggleString", + "value": "", + "toggleValues": ["Hex", "UTF8", "Latin1", "Base64"] + }, + { + "name": "Mode", + "type": "option", + "value": ["CBC", "CFB", "OFB", "CTR", "ECB", "CBC/NoPadding", "ECB/NoPadding"] + }, + { + "name": "Input", + "type": "option", + "value": ["Raw", "Hex"] + }, + { + "name": "Output", + "type": "option", + "value": ["Hex", "Raw"] + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const key = Utils.convertToByteArray(args[0].string, args[0].option), + iv = Utils.convertToByteArray(args[1].string, args[1].option), + [,, mode, inputType, outputType] = args; + + if (key.length !== 16) + throw new OperationError(`Invalid key length: ${key.length} bytes + +SM4 uses a key length of 16 bytes (128 bits).`); + if (iv.length !== 16 && !mode.startsWith("ECB")) + throw new OperationError(`Invalid IV length: ${iv.length} bytes + +SM4 uses an IV length of 16 bytes (128 bits). +Make sure you have specified the type correctly (e.g. Hex vs UTF8).`); + + input = Utils.convertToByteArray(input, inputType); + const output = decryptSM4(input, key, iv, mode.substring(0, 3), mode.endsWith("NoPadding")); + return outputType === "Hex" ? toHex(output) : Utils.byteArrayToUtf8(output); + } + +} + +export default SM4Decrypt; diff --git a/src/core/operations/SM4Encrypt.mjs b/src/core/operations/SM4Encrypt.mjs new file mode 100644 index 00000000..220898a4 --- /dev/null +++ b/src/core/operations/SM4Encrypt.mjs @@ -0,0 +1,88 @@ +/** + * @author swesven + * @copyright 2021 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import Utils from "../Utils.mjs"; +import OperationError from "../errors/OperationError.mjs"; +import { toHex } from "../lib/Hex.mjs"; +import { encryptSM4 } from "../lib/SM4.mjs"; + +/** + * SM4 Encrypt operation + */ +class SM4Encrypt extends Operation { + + /** + * SM4Encrypt constructor + */ + constructor() { + super(); + + this.name = "SM4 Encrypt"; + this.module = "Ciphers"; + this.description = "SM4 is a 128-bit block cipher, currently established as a national standard (GB/T 32907-2016) of China. Multiple block cipher modes are supported. When using CBC or ECB mode, the PKCS#7 padding scheme is used."; + this.infoURL = "https://en.wikipedia.org/wiki/SM4_(cipher)"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + "name": "Key", + "type": "toggleString", + "value": "", + "toggleValues": ["Hex", "UTF8", "Latin1", "Base64"] + }, + { + "name": "IV", + "type": "toggleString", + "value": "", + "toggleValues": ["Hex", "UTF8", "Latin1", "Base64"] + }, + { + "name": "Mode", + "type": "option", + "value": ["CBC", "CFB", "OFB", "CTR", "ECB"] + }, + { + "name": "Input", + "type": "option", + "value": ["Raw", "Hex"] + }, + { + "name": "Output", + "type": "option", + "value": ["Hex", "Raw"] + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const key = Utils.convertToByteArray(args[0].string, args[0].option), + iv = Utils.convertToByteArray(args[1].string, args[1].option), + [,, mode, inputType, outputType] = args; + + if (key.length !== 16) + throw new OperationError(`Invalid key length: ${key.length} bytes + +SM4 uses a key length of 16 bytes (128 bits).`); + if (iv.length !== 16 && !mode.startsWith("ECB")) + throw new OperationError(`Invalid IV length: ${iv.length} bytes + +SM4 uses an IV length of 16 bytes (128 bits). +Make sure you have specified the type correctly (e.g. Hex vs UTF8).`); + + input = Utils.convertToByteArray(input, inputType); + const output = encryptSM4(input, key, iv, mode.substring(0, 3), mode.endsWith("NoPadding")); + return outputType === "Hex" ? toHex(output) : Utils.byteArrayToUtf8(output); + } + +} + +export default SM4Encrypt; diff --git a/src/core/operations/TripleDESDecrypt.mjs b/src/core/operations/TripleDESDecrypt.mjs index c90bf926..b11c2b7f 100644 --- a/src/core/operations/TripleDESDecrypt.mjs +++ b/src/core/operations/TripleDESDecrypt.mjs @@ -42,7 +42,7 @@ class TripleDESDecrypt extends Operation { { "name": "Mode", "type": "option", - "value": ["CBC", "CFB", "OFB", "CTR", "ECB"] + "value": ["CBC", "CFB", "OFB", "CTR", "ECB", "CBC/NoPadding", "ECB/NoPadding"] }, { "name": "Input", @@ -65,7 +65,7 @@ class TripleDESDecrypt extends Operation { run(input, args) { const key = Utils.convertToByteString(args[0].string, args[0].option), iv = Utils.convertToByteArray(args[1].string, args[1].option), - mode = args[2], + mode = args[2].substring(0, 3), inputType = args[3], outputType = args[4]; @@ -85,6 +85,12 @@ Make sure you have specified the type correctly (e.g. Hex vs UTF8).`); input = Utils.convertToByteString(input, inputType); const decipher = forge.cipher.createDecipher("3DES-" + mode, key); + /* Allow for a "no padding" mode */ + if (args[2].endsWith("NoPadding")) { + decipher.mode.unpad = function(output, options) { + return true; + }; + } decipher.start({iv: iv}); decipher.update(forge.util.createBuffer(input)); const result = decipher.finish(); diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index d20ccc61..2d6d3bd7 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -74,6 +74,7 @@ import "./tests/SeqUtils.mjs"; import "./tests/SetDifference.mjs"; import "./tests/SetIntersection.mjs"; import "./tests/SetUnion.mjs"; +import "./tests/SM4.mjs"; import "./tests/StrUtils.mjs"; import "./tests/SymmetricDifference.mjs"; import "./tests/TextEncodingBruteForce.mjs"; diff --git a/tests/operations/tests/SM4.mjs b/tests/operations/tests/SM4.mjs new file mode 100644 index 00000000..3d7bc453 --- /dev/null +++ b/tests/operations/tests/SM4.mjs @@ -0,0 +1,279 @@ +/** + * SM4 crypto tests. + * + * Test data used from IETF draft-ribose-cfrg-sm4-09, see: + * https://tools.ietf.org/id/draft-ribose-cfrg-sm4-09.html + * + * @author swesven + * @copyright 2021 + * @license Apache-2.0 + */ +import TestRegister from "../../lib/TestRegister.mjs"; + +/* Cleartexts */ +const TWO_BLOCK_PLAIN = "aa aa aa aa bb bb bb bb cc cc cc cc dd dd dd dd ee ee ee ee ff ff ff ff aa aa aa aa bb bb bb bb"; +const FOUR_BLOCK_PLAIN = "aa aa aa aa aa aa aa aa bb bb bb bb bb bb bb bb cc cc cc cc cc cc cc cc dd dd dd dd dd dd dd dd ee ee ee ee ee ee ee ee ff ff ff ff ff ff ff ff aa aa aa aa aa aa aa aa bb bb bb bb bb bb bb bb"; +/* Keys */ +const KEY_1 = "01 23 45 67 89 ab cd ef fe dc ba 98 76 54 32 10"; +const KEY_2 = "fe dc ba 98 76 54 32 10 01 23 45 67 89 ab cd ef"; +/* IV */ +const IV = "00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f"; +/* Ciphertexts */ +const ECB_1 = "5e c8 14 3d e5 09 cf f7 b5 17 9f 8f 47 4b 86 19 2f 1d 30 5a 7f b1 7d f9 85 f8 1c 84 82 19 23 04"; +const ECB_2 = "c5 87 68 97 e4 a5 9b bb a7 2a 10 c8 38 72 24 5b 12 dd 90 bc 2d 20 06 92 b5 29 a4 15 5a c9 e6 00"; +/* With PKCS#7 padding */ +const ECB_1P ="5e c8 14 3d e5 09 cf f7 b5 17 9f 8f 47 4b 86 19 2f 1d 30 5a 7f b1 7d f9 85 f8 1c 84 82 19 23 04 00 2a 8a 4e fa 86 3c ca d0 24 ac 03 00 bb 40 d2"; +const ECB_2P= "c5 87 68 97 e4 a5 9b bb a7 2a 10 c8 38 72 24 5b 12 dd 90 bc 2d 20 06 92 b5 29 a4 15 5a c9 e6 00 a2 51 49 20 93 f8 f6 42 89 b7 8d 6e 8a 28 b1 c6"; +const CBC_1 = "78 eb b1 1c c4 0b 0a 48 31 2a ae b2 04 02 44 cb 4c b7 01 69 51 90 92 26 97 9b 0d 15 dc 6a 8f 6d"; +const CBC_2 = "0d 3a 6d dc 2d 21 c6 98 85 72 15 58 7b 7b b5 9a 91 f2 c1 47 91 1a 41 44 66 5e 1f a1 d4 0b ae 38"; +const OFB_1 = "ac 32 36 cb 86 1d d3 16 e6 41 3b 4e 3c 75 24 b7 1d 01 ac a2 48 7c a5 82 cb f5 46 3e 66 98 53 9b"; +const OFB_2 = "5d cc cd 25 a8 4b a1 65 60 d7 f2 65 88 70 68 49 33 fa 16 bd 5c d9 c8 56 ca ca a1 e1 01 89 7a 97"; +const CFB_1 = "ac 32 36 cb 86 1d d3 16 e6 41 3b 4e 3c 75 24 b7 69 d4 c5 4e d4 33 b9 a0 34 60 09 be b3 7b 2b 3f"; +const CFB_2 = "5d cc cd 25 a8 4b a1 65 60 d7 f2 65 88 70 68 49 0d 9b 86 ff 20 c3 bf e1 15 ff a0 2c a6 19 2c c5"; +const CTR_1 = "ac 32 36 cb 97 0c c2 07 91 36 4c 39 5a 13 42 d1 a3 cb c1 87 8c 6f 30 cd 07 4c ce 38 5c dd 70 c7 f2 34 bc 0e 24 c1 19 80 fd 12 86 31 0c e3 7b 92 6e 02 fc d0 fa a0 ba f3 8b 29 33 85 1d 82 45 14"; +const CTR_2 = "5d cc cd 25 b9 5a b0 74 17 a0 85 12 ee 16 0e 2f 8f 66 15 21 cb ba b4 4c c8 71 38 44 5b c2 9e 5c 0a e0 29 72 05 d6 27 04 17 3b 21 23 9b 88 7f 6c 8c b5 b8 00 91 7a 24 88 28 4b de 9e 16 ea 29 06"; + +TestRegister.addTests([ + { + name: "SM4 Encrypt: ECB 1, No padding", + input: TWO_BLOCK_PLAIN, + expectedOutput: ECB_1, + recipeConfig: [ + { + "op": "SM4 Encrypt", + "args": [{string: KEY_1, option: "Hex"}, {string: "", option: "Hex"}, "ECB/NoPadding", "Hex", "Hex"] + } + ] + }, + { + name: "SM4 Encrypt: ECB 2, No padding", + input: TWO_BLOCK_PLAIN, + expectedOutput: ECB_2, + recipeConfig: [ + { + "op": "SM4 Encrypt", + "args": [{string: KEY_2, option: "Hex"}, {string: "", option: "Hex"}, "ECB/NoPadding", "Hex", "Hex"] + } + ] + }, + { + name: "SM4 Encrypt: ECB 1, With padding", + input: TWO_BLOCK_PLAIN, + expectedOutput: ECB_1P, + recipeConfig: [ + { + "op": "SM4 Encrypt", + "args": [{string: KEY_1, option: "Hex"}, {string: "", option: "Hex"}, "ECB", "Hex", "Hex"] + } + ] + }, + { + name: "SM4 Encrypt: ECB 2, With padding", + input: TWO_BLOCK_PLAIN, + expectedOutput: ECB_2P, + recipeConfig: [ + { + "op": "SM4 Encrypt", + "args": [{string: KEY_2, option: "Hex"}, {string: "", option: "Hex"}, "ECB", "Hex", "Hex"] + } + ] + }, + { + name: "SM4 Encrypt: CBC 1", + input: TWO_BLOCK_PLAIN, + expectedOutput: CBC_1, + recipeConfig: [ + { + "op": "SM4 Encrypt", + "args": [{string: KEY_1, option: "Hex"}, {string: IV, option: "Hex"}, "CBC/NoPadding", "Hex", "Hex"] + } + ] + }, + { + name: "SM4 Encrypt: CBC 2", + input: TWO_BLOCK_PLAIN, + expectedOutput: CBC_2, + recipeConfig: [ + { + "op": "SM4 Encrypt", + "args": [{string: KEY_2, option: "Hex"}, {string: IV, option: "Hex"}, "CBC/NoPadding", "Hex", "Hex"] + } + ] + }, + { + name: "SM4 Encrypt: OFB1", + input: TWO_BLOCK_PLAIN, + expectedOutput: OFB_1, + recipeConfig: [ + { + "op": "SM4 Encrypt", + "args": [{string: KEY_1, option: "Hex"}, {string: IV, option: "Hex"}, "OFB", "Hex", "Hex"] + } + ] + }, + { + name: "SM4 Encrypt: OFB2", + input: TWO_BLOCK_PLAIN, + expectedOutput: OFB_2, + recipeConfig: [ + { + "op": "SM4 Encrypt", + "args": [{string: KEY_2, option: "Hex"}, {string: IV, option: "Hex"}, "OFB", "Hex", "Hex"] + } + ] + }, + { + name: "SM4 Encrypt: CFB1", + input: TWO_BLOCK_PLAIN, + expectedOutput: CFB_1, + recipeConfig: [ + { + "op": "SM4 Encrypt", + "args": [{string: KEY_1, option: "Hex"}, {string: IV, option: "Hex"}, "CFB", "Hex", "Hex"] + } + ] + }, + { + name: "SM4 Encrypt: CFB2", + input: TWO_BLOCK_PLAIN, + expectedOutput: CFB_2, + recipeConfig: [ + { + "op": "SM4 Encrypt", + "args": [{string: KEY_2, option: "Hex"}, {string: IV, option: "Hex"}, "CFB", "Hex", "Hex"] + } + ] + }, + { + name: "SM4 Encrypt: CTR1", + input: FOUR_BLOCK_PLAIN, + expectedOutput: CTR_1, + recipeConfig: [ + { + "op": "SM4 Encrypt", + "args": [{string: KEY_1, option: "Hex"}, {string: IV, option: "Hex"}, "CTR", "Hex", "Hex"] + } + ] + }, + { + name: "SM4 Encrypt: CTR1", + input: FOUR_BLOCK_PLAIN, + expectedOutput: CTR_2, + recipeConfig: [ + { + "op": "SM4 Encrypt", + "args": [{string: KEY_2, option: "Hex"}, {string: IV, option: "Hex"}, "CTR", "Hex", "Hex"] + } + ] + }, + { + name: "SM4 Decrypt: ECB 1", + input: ECB_1, + expectedOutput: TWO_BLOCK_PLAIN, + recipeConfig: [ + { + "op": "SM4 Decrypt", + "args": [{string: KEY_1, option: "Hex"}, {string: "", option: "Hex"}, "ECB/NoPadding", "Hex", "Hex"] + } + ] + }, + { + name: "SM4 Decrypt: ECB 2", + input: ECB_2, + expectedOutput: TWO_BLOCK_PLAIN, + recipeConfig: [ + { + "op": "SM4 Decrypt", + "args": [{string: KEY_2, option: "Hex"}, {string: "", option: "Hex"}, "ECB/NoPadding", "Hex", "Hex"] + } + ] + }, + { + name: "SM4 Decrypt: CBC 1", + input: CBC_1, + expectedOutput: TWO_BLOCK_PLAIN, + recipeConfig: [ + { + "op": "SM4 Decrypt", + "args": [{string: KEY_1, option: "Hex"}, {string: IV, option: "Hex"}, "CBC/NoPadding", "Hex", "Hex"] + } + ] + }, + { + name: "SM4 Decrypt: CBC 2", + input: CBC_2, + expectedOutput: TWO_BLOCK_PLAIN, + recipeConfig: [ + { + "op": "SM4 Decrypt", + "args": [{string: KEY_2, option: "Hex"}, {string: IV, option: "Hex"}, "CBC/NoPadding", "Hex", "Hex"] + } + ] + }, + { + name: "SM4 Decrypt: OFB1", + input: TWO_BLOCK_PLAIN, + expectedOutput: OFB_1, + recipeConfig: [ + { + "op": "SM4 Decrypt", + "args": [{string: KEY_1, option: "Hex"}, {string: IV, option: "Hex"}, "OFB", "Hex", "Hex"] + } + ] + }, + { + name: "SM4 Decrypt: OFB2", + input: OFB_2, + expectedOutput: TWO_BLOCK_PLAIN, + recipeConfig: [ + { + "op": "SM4 Decrypt", + "args": [{string: KEY_2, option: "Hex"}, {string: IV, option: "Hex"}, "OFB", "Hex", "Hex"] + } + ] + }, + { + name: "SM4 Decrypt: CFB1", + input: CFB_1, + expectedOutput: TWO_BLOCK_PLAIN, + recipeConfig: [ + { + "op": "SM4 Decrypt", + "args": [{string: KEY_1, option: "Hex"}, {string: IV, option: "Hex"}, "CFB", "Hex", "Hex"] + } + ] + }, + { + name: "SM4 Decrypt: CFB2", + input: CFB_2, + expectedOutput: TWO_BLOCK_PLAIN, + recipeConfig: [ + { + "op": "SM4 Decrypt", + "args": [{string: KEY_2, option: "Hex"}, {string: IV, option: "Hex"}, "CFB", "Hex", "Hex"] + } + ] + }, + { + name: "SM4 Decrypt: CTR1", + input: CTR_1, + expectedOutput: FOUR_BLOCK_PLAIN, + recipeConfig: [ + { + "op": "SM4 Decrypt", + "args": [{string: KEY_1, option: "Hex"}, {string: IV, option: "Hex"}, "CTR", "Hex", "Hex"] + } + ] + }, + { + name: "SM4 Decrypt: CTR1", + input: CTR_2, + expectedOutput: FOUR_BLOCK_PLAIN, + recipeConfig: [ + { + "op": "SM4 Decrypt", + "args": [{string: KEY_2, option: "Hex"}, {string: IV, option: "Hex"}, "CTR", "Hex", "Hex"] + } + ] + }, +]);