diff --git a/CHANGELOG.md b/CHANGELOG.md index ee286d4..629bbd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # CHANGELOG +## 1.3.0 + +asymmetric encryption and signatures + +object signing: ability to use rsa keys + ## 1.2.0 object signing: diff --git a/README.md b/README.md index ad0f763..f8d9c5e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # @sapphirecode/crypto-helper -version: 1.2.x +version: 1.3.x simple functions for cryptography @@ -12,10 +12,12 @@ npm: yarn: -> yarn add @sapphirecode/crypto-helper +> yarn add @sapphirecode/crypto-helper ## Usage +### Examples + ```js const crypto = require('@sapphirecode/crypto-helper'); @@ -31,13 +33,34 @@ const info = crypto.get_signature_info(signed); // returns an object with iat (i const dec = crypto.decode_signed(signed); // decode a signed object without verifying the signature const ver = crypto.verify_signature(signed, 'secret', 10000); // verifies the signature and returns the contents. the timeout is in milliseconds and optional, timing will be ignored if omitted. const ver_info = crypto.verify_signature_get_info(signed, 'secret', 10000); // verify a signature and get signature information like iat and key_info -const ver_func = crypto.verify_signature(signed, (signature_info)=>'secret', 10000); // verify a signature, retrieve the key using the signature info +const ver_func = crypto.verify_signature( + signed, + (signature_info) => 'secret', + 10000 +); // verify a signature, retrieve the key using the signature info // encryption const enc = crypto.encrypt_aes('foo', 'bar'); const dec = crypto.decrypt_aes(enc, 'bar'); + +// asymmetric encryption and signatures +const keys = await crypto.generate_keypair(2048); // generate private and public key (length is optional and 2048 by default) + +const aenc = crypto.asym_encrypt('foo', keys.public_key); // encrypt +const adec = crypto.asym_decrypt(aenc, key.private_key); // decrypt + +const asig = crypto.asym_sign('foo', keys.private_key); // create signature +const aver = crypto.asym_verify('foo', keys.public_key, asig); // verify signature, returns boolean ``` +### Asymmetric signatures on object signing + +the functions `sign_object`, `verify_signature`, ... will automatically detect +rsa keys and use them to sign objects asymmetrically. Note that keys have to be +provided in the correct order (private key for signing, public key for +verifying). Else the keys will just be interpreted as symmetric and verification +will fail. + ## License MIT © Timo Hocker diff --git a/index.js b/index.js index 08429c4..1995151 100644 --- a/index.js +++ b/index.js @@ -8,293 +8,16 @@ // @ts-nocheck 'use strict'; -const crypto = require ('crypto'); -const encoding = require ('@sapphirecode/encoding-helper'); - -const encryption_mode_cbc_256 = { - algorithm: 'aes-256-cbc', - nonce_size: 16, - key_size: 32, - hash: 'sha256', - salt_size: 16, - iterations: 32767 -}; - -const encryption_mode_cbc_256_quick = { - algorithm: 'aes-256-cbc', - nonce_size: 16, - key_size: 32, - hash: 'sha256', - salt_size: 16, - iterations: 32 -}; - -const encryption_mode_cbc_128 = { - algorithm: 'aes-128-cbc', - nonce_size: 16, - key_size: 16, - hash: 'sha256', - salt_size: 16, - iterations: 40 -}; - -/** - * creates a random string - * - * @param {number} len string length default: 8 - * @returns {string} random string - */ -function random_string (len = 8) { - if (len < 1) - throw new Error ('invalid length'); - return crypto.randomBytes (Math.ceil (len * 3 / 4)) - .toString ('base64') - .substr (0, len); -} - -/** - * creates a random hexadecimal string - * - * @param {number} len length - * @returns {string} hex string - */ -function random_hex (len = 8) { - if (len < 1) - throw new Error ('invalid length'); - return crypto.randomBytes (Math.ceil (len / 2)) - .toString ('hex') - .substr (0, len); -} - -/** - * creates a 64 character long random hex string - * - * @returns {string} salt - */ -function create_salt () { - return random_hex (64); -} - -/** - * creates a sha512 hash - * - * @param {string} str string input - * @param {string} salt salt - * @returns {string} salt - */ -function hash_sha512 (str, salt) { - const md = crypto.createHash ('sha512'); - md.update (str); - md.update (salt); - return md.digest ('hex'); -} - -/** - * sign an object - * - * @param {any} obj object to sign - * @param {string} key key to use - * @param {string|Object} key_info key identifier - * @returns {string} signed object - */ -function sign_object (obj, key, key_info = null) { - const payload = { - iat: Date.now (), - obj, - ...(typeof key_info === 'object' ? key_info : { key_info }) - }; - const str = encoding.to_b58 (JSON.stringify (payload)); - const token = encoding.to_b58 (hash_sha512 (str, key), 'hex'); - const res = `${str}.${token}.2`; - return res; -} - -function parse_signature (str, key = null) { - let dec = str.split ('.'); - const version = dec[2]; - const res = {}; - switch (version) { - case '2': - res.json = JSON.parse (encoding.to_utf8 (dec[0], 'base58')); - res.token = encoding.to_hex (dec[1], 'base58'); - break; - default: - dec = decodeURIComponent (str) - .split ('.'); - res.json = JSON.parse ( - encoding.to_utf8 (dec[0], 'base64') - ); - res.token = encoding.to_hex (dec[1], 'base64'); - break; - } - - if (key !== null) { - const string_key = typeof key === 'string' ? key : key (res.json); - res.hash = hash_sha512 (dec[0], string_key); - } - return res; -} - -/** - * verify a signed object and return its info and contents - * - * @param {string} str string to verify - * @param {string|(Object)=>string} key used key - * @param {number|(Object)=>number} timeout timeout (optional) - * @returns {any} returns object if successful, else null - */ -function verify_signature_get_info (str, key, timeout = 0) { - if (typeof str !== 'string') - return null; - const { json, token, hash } = parse_signature (str, key); - if (token !== hash) - return null; - const time = Date.now () - json.iat; - const num_timeout = typeof timeout === 'number' ? timeout : timeout (json); - if (num_timeout === 0 || time <= num_timeout) - return json; - return null; -} - -/** - * verify a signed object and return its contents - * - * @param {string} str string to verify - * @param {string|(Object)=>string} key used key - * @param {number|(Object)=>number} timeout timeout (optional) - * @returns {any} returns object if successful, else null - */ -function verify_signature (str, key, timeout = 0) { - const res = verify_signature_get_info (str, key, timeout); - if (res === null) - return null; - return res.obj; -} - -/** - * get a signed object info and data - * - * @param {string} str string to decode - * @returns {any} data - */ -function get_signature_info (str) { - if (typeof str !== 'string') - return null; - const { json } = parse_signature (str); - return json; -} - -/** - * decode a signed object without verifying the signature - * - * @param {string} str string to decode - * @returns {any} object - */ -function decode_signed (str) { - const info = get_signature_info (str); - if (info) - return info.obj; - return null; -} - -/** - * creates a sha256 hash - * - * @param {any} data input - * @returns {string} hash - */ -function checksum (data) { - const md = crypto.createHash ('sha256'); - md.update (String (data)); - return md.digest ('hex'); -} - -/** - * encrypt plain text with aes - * - * @param {string} text plaintext - * @param {string} pass password - * @param {object} mode encryption mode - * @returns {string} encrypted - */ -function encrypt_aes (text, pass, mode = encryption_mode_cbc_256) { - const salt = crypto.randomBytes (mode.salt_size); - // eslint-disable-next-line no-sync - const key = crypto.pbkdf2Sync ( - Buffer.from (pass), - salt, - mode.iterations, - mode.key_size, - mode.hash - ); - const nonce = crypto.randomBytes (mode.nonce_size); - const cipher = crypto.createCipheriv (mode.algorithm, key, nonce); - return Buffer.concat ([ - salt, - nonce, - cipher.update (Buffer.from (text)), - cipher.final () - ]) - .toString ('base64'); -} - -/** - * decrypt an aes string - * - * @param {string} ciphertext encrypted text - * @param {string} pass password - * @param {object} mode encryption mode - * @param {boolean} rethrow rethrow exceptions instead of returning null - * @returns {string} plaintext - */ -function decrypt_aes ( - ciphertext, - pass, - mode = encryption_mode_cbc_256, - rethrow = false -) { - try { - let buf = Buffer.from (ciphertext, 'base64'); - const salt = buf.slice (0, mode.salt_size); - buf = buf.slice (mode.salt_size); - // eslint-disable-next-line no-sync - const key = crypto.pbkdf2Sync ( - Buffer.from (pass), - salt, - mode.iterations, - mode.key_size, - mode.hash - ); - const nonce = buf.slice (0, mode.nonce_size); - buf = buf.slice (mode.nonce_size); - const cipher = crypto.createDecipheriv (mode.algorithm, key, nonce); - return Buffer.concat ([ - cipher.update (buf), - cipher.final () - ]) - .toString (); - } - catch (e) { - if (rethrow) - throw e; - } - return null; -} +const encryption = require ('./lib/encryption'); +const hashing = require ('./lib/hashing'); +const random = require ('./lib/random'); +const signatures = require ('./lib/signatures'); +const rsa = require ('./lib/rsa'); module.exports = { - checksum, - create_salt, - decode_signed, - decrypt_aes, - encrypt_aes, - encryption_mode_cbc_128, - encryption_mode_cbc_256, - encryption_mode_cbc_256_quick, - get_signature_info, - hash_sha512, - random_hex, - random_string, - sign_object, - verify_signature, - verify_signature_get_info + ...random, + ...hashing, + ...encryption, + ...signatures, + ...rsa }; diff --git a/lib/encryption.js b/lib/encryption.js new file mode 100644 index 0000000..16e05bf --- /dev/null +++ b/lib/encryption.js @@ -0,0 +1,110 @@ +'use strict'; + +const crypto = require ('crypto'); + +const encryption_mode_cbc_256 = { + algorithm: 'aes-256-cbc', + nonce_size: 16, + key_size: 32, + hash: 'sha256', + salt_size: 16, + iterations: 32767 +}; + +const encryption_mode_cbc_256_quick = { + algorithm: 'aes-256-cbc', + nonce_size: 16, + key_size: 32, + hash: 'sha256', + salt_size: 16, + iterations: 32 +}; + +const encryption_mode_cbc_128 = { + algorithm: 'aes-128-cbc', + nonce_size: 16, + key_size: 16, + hash: 'sha256', + salt_size: 16, + iterations: 40 +}; + +/** + * encrypt plain text with aes + * + * @param {string} text plaintext + * @param {string} pass password + * @param {object} mode encryption mode + * @returns {string} encrypted + */ +function encrypt_aes (text, pass, mode = encryption_mode_cbc_256) { + const salt = crypto.randomBytes (mode.salt_size); + // eslint-disable-next-line no-sync + const key = crypto.pbkdf2Sync ( + Buffer.from (pass), + salt, + mode.iterations, + mode.key_size, + mode.hash + ); + const nonce = crypto.randomBytes (mode.nonce_size); + const cipher = crypto.createCipheriv (mode.algorithm, key, nonce); + return Buffer.concat ([ + salt, + nonce, + cipher.update (Buffer.from (text)), + cipher.final () + ]) + .toString ('base64'); +} + +/** + * decrypt an aes string + * + * @param {string} ciphertext encrypted text + * @param {string} pass password + * @param {object} mode encryption mode + * @param {boolean} rethrow rethrow exceptions instead of returning null + * @returns {string} plaintext + */ +function decrypt_aes ( + ciphertext, + pass, + mode = encryption_mode_cbc_256, + rethrow = false +) { + try { + let buf = Buffer.from (ciphertext, 'base64'); + const salt = buf.slice (0, mode.salt_size); + buf = buf.slice (mode.salt_size); + // eslint-disable-next-line no-sync + const key = crypto.pbkdf2Sync ( + Buffer.from (pass), + salt, + mode.iterations, + mode.key_size, + mode.hash + ); + const nonce = buf.slice (0, mode.nonce_size); + buf = buf.slice (mode.nonce_size); + const cipher = crypto.createDecipheriv (mode.algorithm, key, nonce); + return Buffer.concat ([ + cipher.update (buf), + cipher.final () + ]) + .toString (); + } + catch (e) { + if (rethrow) + throw e; + } + return null; +} + +module.exports = { + decrypt_aes, + encrypt_aes, + encryption_mode_cbc_128, + encryption_mode_cbc_256, + encryption_mode_cbc_256_quick +}; diff --git a/lib/hashing.js b/lib/hashing.js new file mode 100644 index 0000000..1499118 --- /dev/null +++ b/lib/hashing.js @@ -0,0 +1,34 @@ +'use strict'; + +const crypto = require ('crypto'); + +/** + * creates a sha256 hash + * + * @param {any} data input + * @returns {string} hash + */ +function checksum (data) { + const md = crypto.createHash ('sha256'); + md.update (String (data)); + return md.digest ('hex'); +} + +/** + * creates a sha512 hash + * + * @param {string} str string input + * @param {string} salt salt + * @returns {string} salt + */ +function hash_sha512 (str, salt) { + const md = crypto.createHash ('sha512'); + md.update (str); + md.update (salt); + return md.digest ('hex'); +} + +module.exports = { + hash_sha512, + checksum +}; diff --git a/lib/random.js b/lib/random.js new file mode 100644 index 0000000..44c5a62 --- /dev/null +++ b/lib/random.js @@ -0,0 +1,46 @@ +'use strict'; + +const crypto = require ('crypto'); + +/** + * creates a random string + * + * @param {number} len string length default: 8 + * @returns {string} random string + */ +function random_string (len = 8) { + if (len < 1) + throw new Error ('invalid length'); + return crypto.randomBytes (Math.ceil (len * 3 / 4)) + .toString ('base64') + .substr (0, len); +} + +/** + * creates a random hexadecimal string + * + * @param {number} len length + * @returns {string} hex string + */ +function random_hex (len = 8) { + if (len < 1) + throw new Error ('invalid length'); + return crypto.randomBytes (Math.ceil (len / 2)) + .toString ('hex') + .substr (0, len); +} + +/** + * creates a 64 character long random hex string + * + * @returns {string} salt + */ +function create_salt () { + return random_hex (64); +} + +module.exports = { + create_salt, + random_hex, + random_string +}; diff --git a/lib/rsa.js b/lib/rsa.js new file mode 100644 index 0000000..4793ffc --- /dev/null +++ b/lib/rsa.js @@ -0,0 +1,98 @@ +'use strict'; + +const crypto = require ('crypto'); + +/** + * generate a new rsa keypair + * + * @param {number} length the key length in bit. default: 2048 + * @returns {Promise<{public_key: string, private_key: string}>} generated keys + */ +async function generate_keypair (length = 2048) { + const key = await new Promise ( + (res, rej) => crypto.generateKeyPair ( + 'rsa', + { + modulusLength: length, + publicKeyEncoding: { + type: 'pkcs1', + format: 'pem' + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'pem' + } + }, + (err, public_key, private_key) => { + if (err) + rej (err); + res ({ public_key, private_key }); + } + ) + ); + + return key; +} + +/** + * encrypts data using a public key + * it can only be decrypted with the corresponding private key + * + * @param {string} data data to encrypt + * @param {string} public_key public key + * @returns {string} encrypted data + */ +function asym_encrypt (data, public_key) { + return crypto.publicEncrypt (public_key, Buffer.from (data)) + .toString ('base64'); +} + +/** + * decrypts data using a private key + * + * @param {string} data data to decrypt + * @param {string} private_key private key + * @returns {string} decrypted data + */ +function asym_decrypt (data, private_key) { + return crypto.privateDecrypt (private_key, Buffer.from (data, 'base64')) + .toString (); +} + +/** + * creates a signature using a private key + * can later be verified using the corresponding public key + * + * @param {string} data data to sign + * @param {string} private_key private key + * @returns {string} signature + */ +function asym_sign (data, private_key) { + const sign = crypto.createSign ('sha256'); + sign.write (data); + sign.end (); + return sign.sign (private_key, 'hex'); +} + +/** + * verifies a signature using a public key + * + * @param {string} data data to verify + * @param {string} public_key public key + * @param {string} signature signature to verify + * @returns {boolean} true if signature is valid + */ +function asym_verify (data, public_key, signature) { + const verify = crypto.createVerify ('sha256'); + verify.write (data); + verify.end (); + return verify.verify (public_key, signature, 'hex'); +} + +module.exports = { + generate_keypair, + asym_encrypt, + asym_decrypt, + asym_sign, + asym_verify +}; diff --git a/lib/signatures.js b/lib/signatures.js new file mode 100644 index 0000000..4614e89 --- /dev/null +++ b/lib/signatures.js @@ -0,0 +1,126 @@ +'use strict'; + +const encoding = require ('@sapphirecode/encoding-helper'); +const { hash_sha512 } = require ('./hashing'); +const { asym_sign, asym_verify } = require ('./rsa'); + +/** + * sign an object + * + * @param {any} obj object to sign + * @param {string} key key to use + * @param {string|Object} key_info key identifier + * @returns {string} signed object + */ +function sign_object (obj, key, key_info = null) { + const payload = { + iat: Date.now (), + obj, + ...(typeof key_info === 'object' ? key_info : { key_info }) + }; + const str = encoding.to_b58 (JSON.stringify (payload)); + const is_rsa = (/^-----BEGIN PRIVATE KEY-----/u).test (key); + const signature = is_rsa ? asym_sign (str, key) : hash_sha512 (str, key); + const token = encoding.to_b58 (signature, 'hex'); + const res = `${str}.${token}.2`; + return res; +} + +function parse_signature (str, key = null) { + let dec = str.split ('.'); + const version = dec[2]; + const res = {}; + switch (version) { + case '2': + res.json = JSON.parse (encoding.to_utf8 (dec[0], 'base58')); + res.token = encoding.to_hex (dec[1], 'base58'); + break; + default: + dec = decodeURIComponent (str) + .split ('.'); + res.json = JSON.parse ( + encoding.to_utf8 (dec[0], 'base64') + ); + res.token = encoding.to_hex (dec[1], 'base64'); + break; + } + + if (key !== null) { + const string_key = typeof key === 'string' ? key : key (res.json); + res.is_rsa = (/^-----BEGIN RSA PUBLIC KEY-----/u).test (string_key); + res.hash = res.is_rsa + ? asym_verify (dec[0], string_key, res.token) + : hash_sha512 (dec[0], string_key); + } + return res; +} + +/** + * verify a signed object and return its info and contents + * + * @param {string} str string to verify + * @param {string|(Object)=>string} key used key + * @param {number|(Object)=>number} timeout timeout (optional) + * @returns {any} returns object if successful, else null + */ +function verify_signature_get_info (str, key, timeout = 0) { + if (typeof str !== 'string') + return null; + const { json, token, hash, is_rsa } = parse_signature (str, key); + if (is_rsa ? !hash : (token !== hash)) + return null; + const time = Date.now () - json.iat; + const num_timeout = typeof timeout === 'number' ? timeout : timeout (json); + if (num_timeout === 0 || time <= num_timeout) + return json; + return null; +} + +/** + * verify a signed object and return its contents + * + * @param {string} str string to verify + * @param {string|(Object)=>string} key used key + * @param {number|(Object)=>number} timeout timeout (optional) + * @returns {any} returns object if successful, else null + */ +function verify_signature (str, key, timeout = 0) { + const res = verify_signature_get_info (str, key, timeout); + if (res === null) + return null; + return res.obj; +} + +/** + * get a signed object info and data + * + * @param {string} str string to decode + * @returns {any} data + */ +function get_signature_info (str) { + if (typeof str !== 'string') + return null; + const { json } = parse_signature (str); + return json; +} + +/** + * decode a signed object without verifying the signature + * + * @param {string} str string to decode + * @returns {any} object + */ +function decode_signed (str) { + const info = get_signature_info (str); + if (info) + return info.obj; + return null; +} + +module.exports = { + decode_signed, + get_signature_info, + sign_object, + verify_signature, + verify_signature_get_info +}; diff --git a/package.json b/package.json index 6d211bd..2129e21 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sapphirecode/crypto-helper", - "version": "1.2.2", + "version": "1.3.0", "main": "index.js", "author": { "name": "Timo Hocker", @@ -38,10 +38,12 @@ }, "files": [ "LICENSE", + "lib/*.js", + "lib/*.d.ts", "index.js", "index.d.ts" ], "engines": { - "node": ">=10.0.0" + "node": ">=10.12.0" } } diff --git a/stryker.conf.js b/stryker.conf.js index 80732f4..49db858 100644 --- a/stryker.conf.js +++ b/stryker.conf.js @@ -19,5 +19,5 @@ module.exports = { testRunner: 'jasmine', jasmineConfigFile: 'jasmine.json', coverageAnalysis: 'perTest', - mutate: [ 'index.js' ] + mutate: [ 'lib/*.js' ] }; diff --git a/test/spec/hashing.js b/test/spec/hashing.js new file mode 100644 index 0000000..faf5354 --- /dev/null +++ b/test/spec/hashing.js @@ -0,0 +1,27 @@ +'use strict'; + +const crypto = require ('../../index'); + +describe ('hashing', () => { + it ('sha512', () => { + const hash = crypto.hash_sha512 ('a', 'b'); + expect ( + hash + ) + .toEqual ( + // eslint-disable-next-line max-len + '2d408a0717ec188158278a796c689044361dc6fdde28d6f04973b80896e1823975cdbf12eb63f9e0591328ee235d80e9b5bf1aa6a44f4617ff3caf6400eb172d' + ); + }); + + it ('checksum', () => { + const hash = crypto.checksum ('foo'); + expect ( + hash + ) + .toEqual ( + // eslint-disable-next-line max-len + '2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae' + ); + }); +}); diff --git a/test/spec/random.js b/test/spec/random.js new file mode 100644 index 0000000..3e8e7ea --- /dev/null +++ b/test/spec/random.js @@ -0,0 +1,72 @@ +'use strict'; + +const crypto = require ('../../index'); + +// eslint-disable-next-line max-lines-per-function +describe ('random', () => { + it ('random_hex', () => { + const hex = crypto.random_hex (16); + expect (hex.length) + .toEqual (16); + expect (hex) + .toMatch (/^[0-9a-f]+$/iu); + }); + + it ('random_hex with default length', () => { + const hex = crypto.random_hex (); + expect (hex.length) + .toEqual (8); + expect (hex) + .toMatch (/^[0-9a-f]+$/iu); + }); + + it ('random_hex should refuse length smaller 1', () => { + expect ( + () => (crypto.random_hex (0)) + ) + .toThrowError ('invalid length'); + }); + + it ('random_hex should always return correct length', () => { + for (let i = 1; i < 32; i++) { + const hex = crypto.random_hex (i); + expect (hex.length) + .toEqual (i); + } + }); + + it ('random_string', () => { + const str = crypto.random_string (16); + expect (str.length) + .toEqual (16); + }); + + it ('random_string with default length', () => { + const str = crypto.random_string (); + expect (str.length) + .toEqual (8); + }); + + it ('random_string should refuse length smaller 1', () => { + expect ( + () => (crypto.random_string (0)) + ) + .toThrowError ('invalid length'); + }); + + it ('random_string should always return correct length', () => { + for (let i = 1; i < 32; i++) { + const str = crypto.random_string (i); + expect (str.length) + .toEqual (i); + } + }); + + it ('create_salt', () => { + const salt = crypto.create_salt (); + expect (salt.length) + .toEqual (64); + expect (salt) + .toMatch (/^[0-9a-f]+$/iu); + }); +}); diff --git a/test/spec/rsa.js b/test/spec/rsa.js new file mode 100644 index 0000000..5daf971 --- /dev/null +++ b/test/spec/rsa.js @@ -0,0 +1,133 @@ +'use strict'; + +const crypto = require ('../../index'); + +const key_length = 512; + +// eslint-disable-next-line max-lines-per-function +describe ('rsa', () => { + it ('should create a keypair', async () => { + const k = await crypto.generate_keypair (); + + expect (k.private_key) + .toMatch (/^-----BEGIN PRIVATE KEY-----.+-----END PRIVATE KEY-----\n$/su); + expect (k.public_key) + // eslint-disable-next-line max-len + .toMatch (/^-----BEGIN RSA PUBLIC KEY-----.+-----END RSA PUBLIC KEY-----\n$/su); + }); + + it ('should throw on too small key size', async () => { + await expectAsync (crypto.generate_keypair (16)) + .toBeRejectedWithError ( + 'error:0408F078:rsa routines:pkey_rsa_ctrl:key size too small' + ); + }); + + it ('should encrypt and decrypt', async () => { + const k = await crypto.generate_keypair (key_length); + const data = 'foobar'; + const enc = crypto.asym_encrypt (data, k.public_key); + const dec = crypto.asym_decrypt (enc, k.private_key); + expect (dec) + .toEqual (data); + expect (enc).not.toEqual (data); + }); + + it ('should throw if encryption key is invalid', () => { + const key = '-----BEGIN RSA PUBLIC KEY-----\n' + + 'MEgCCQDGvKLaq1SB/BTzocR4ZqGNr8dz1ylxxUpDncCu0C/ayOGPnCilB0LGEdqK\n' + + 'rORlsYpIaDvLv6x6k8iSg6A/TsybAgMBAAE=\n' + + '-----END RSA PUBLIC KEY-----\n'; + const data = 'foobar'; + expect (() => crypto.asym_encrypt (data, key)) + .toThrow (); + }); + + it ('should throw on invalid decryption key', () => { + const data = 'EHsGr2eSVqZKTX1U9Qj4crGRFkk299kmxhiiO3fyaIS' + + 'olB9+UYDdqrwcf/INwNddW4AzjA2Kf4dYaXIRLZsU1Q=='; + const key = '-----BEGIN PRIVATE KEY-----\n' + + 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAxryi2qtUgfwU86HE\n' + + 'eGahja/Hc9cpccVKQ53ArtXv2sjhj5wopQdCxhHaiqzkZbGKSGg7y7+sepPIkoOg\n' + + 'P07MmwIDAQABAkBKIk3hoi24+07ZfwuqGibDksGlLarxHLZSOMOKsnBXfRRyGqMr\n' + + '/Z+qtQ9VPRWHBzGHZ9rXAVKa8gnRirik+ez5AiEA/XL6tOy92Yvxm46ewswmZ7ab\n' + + 'V1KvChsXziaRj5eLzacCIQDIvLBO8og5Ng4r7E/dOAYrvzOLFqlN5UCuCRZzuFpv\n' + + '7QIgCnj5ywgNQDP8I8Vc4ge1fouZF56fBPfhn+8QDLLiX/kCIHewKd+otJiIJoMB\n' + + '78yTLvq+klkINgKAAsTCHmT5MtMxAiEAkFE70ms8C73JvTkd0znq8H6fBJV0iZxQ\n' + + 'qOXON8bzv8A=\n' + + '-----END PRIVATE KEY-----\n'; + expect (() => crypto.asym_decrypt (data, key)) + .toThrow (); + }); + + it ('should throw on invalid decryption data', () => { + const data = 'EHsGr2eSkqZKTX1U9Qj4crGRFkk299kmxhiiO3fyaIS' + + 'olB9+UYDdqrwcf/INwNddW4AzjA2Kf4dYaXIRLZsU1Q=='; + const key = '-----BEGIN PRIVATE KEY-----\n' + + 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAxryi2qtUgfwU86HE\n' + + 'eGahja/Hc9cpccVKQ53ArtAv2sjhj5wopQdCxhHaiqzkZbGKSGg7y7+sepPIkoOg\n' + + 'P07MmwIDAQABAkBKIk3hoi24+07ZfwuqGibDksGlLarxHLZSOMOKsnBXfRRyGqMr\n' + + '/Z+qtQ9VPRWHBzGHZ9rXAVKa8gnRirik+ez5AiEA/XL6tOy92Yvxm46ewswmZ7ab\n' + + 'V1KvChsXziaRj5eLzacCIQDIvLBO8og5Ng4r7E/dOAYrvzOLFqlN5UCuCRZzuFpv\n' + + '7QIgCnj5ywgNQDP8I8Vc4ge1fouZF56fBPfhn+8QDLLiX/kCIHewKd+otJiIJoMB\n' + + '78yTLvq+klkINgKAAsTCHmT5MtMxAiEAkFE70ms8C73JvTkd0znq8H6fBJV0iZxQ\n' + + 'qOXON8bzv8A=\n' + + '-----END PRIVATE KEY-----\n'; + expect (() => crypto.asym_decrypt (data, key)) + .toThrow (); + }); + + it ('should create a signature and verify it', async () => { + const data = 'foobar'; + const k = await crypto.generate_keypair (key_length); + const signature = crypto.asym_sign (data, k.private_key); + expect (typeof signature) + .toEqual ('string'); + expect (signature) + .toMatch (/^[a-f0-9]+$/ui); + expect (crypto.asym_verify (data, k.public_key, signature)) + .toBeTrue (); + }); + + it ('should throw on invalid sign key', () => { + const key = '-----BEGIN PRIVATE KEY-----\n' + + 'MIIBUwIBADANBgkqhkiG9w0BAQEFAASCAT0wggE5AgEAAkEAl4RZveQ8IXHPZTUf\n' + + 'djwgDxIB5444yFvhIRasdasdasdApz+FmsXxGCatnPUCIHO9P1EfxasdaIq/Lpng\n' + + 'y2ZQh2IaDYxC7K7tvGZwSY/CEcGfAiAcNgsg5ZMIQOWxn2KbFM81ne3b5FzD3Ozh\n' + + 'GxItcBfKuw==\n' + + '-----END PRIVATE KEY-----\n'; + const data = 'foobar'; + expect (() => crypto.asym_sign (data, key)) + .toThrow (); + }); + + it ('should throw on invalid verify key', () => { + const key = '-----BEGIN RSA PUBLIC KEY-----\n' + + 'MEgCQQCXhFm95DwDcc9lNasdasdasdasdasdHhEqigZupsWUxX/3tKn9tvEKV/\n' + + 'zjoENWN63aorN+O+5MxDZMnEk+z9AgMBAAE=\n' + + '-----END RSA PUBLIC KEY-----\n'; + const data = 'foobar'; + const signature = '3433ef165d6be80430e1107be0d7183bee769dbb38ea7d2737' + + '1429c853fcab78bf1d3256fc16ee93a38cfd4ae79a74748e59fe9e9a65400c720d' + + 'adb2dbcc1fa3'; + expect (() => crypto.asym_verify (data, key, signature)) + .toThrow (); + }); + + it ('should not throw on invalid data or signature', () => { + const key = '-----BEGIN RSA PUBLIC KEY-----\n' + + 'MEgCQQC+9nw80cG+AsZ2euIZx4ptmUykJgEUgs4JiEvC+IaiIRAd9zGc0TcQAeND\n' + + '171lw77kE02KJ4ARl1hkcLCW2bIrAgMBAAE=\n' + + '-----END RSA PUBLIC KEY-----\n'; + const data = 'foobar'; + const signature = '6bae51b0449d71ca2e04099268bd0f6506a5dfc6f810bd72d' + + '47865574a99910e404e5856da650cd45ee88365a2511fcc0866a0b5d1faf15c067' + + 'ab8a4427554bf'; + expect (crypto.asym_verify (data, key, signature)) + .toBeTrue (); + expect (crypto.asym_verify (data.replace ('b', 'c'), key, signature)) + .toBeFalse (); + expect (crypto.asym_verify (data, key, signature.replace ('f', 'e'))) + .toBeFalse (); + }); +}); diff --git a/test/spec/index.js b/test/spec/signatures.js similarity index 67% rename from test/spec/index.js rename to test/spec/signatures.js index 75c7fe9..81e36e2 100644 --- a/test/spec/index.js +++ b/test/spec/signatures.js @@ -1,17 +1,10 @@ -/* - * Copyright (C) Sapphirecode - All Rights Reserved - * This file is part of crypto-helper which is released under MIT. - * See file 'LICENSE' for full license details. - * Created by Timo Hocker , May 2020 - */ - -// @ts-nocheck 'use strict'; const crypto = require ('../../index'); +const rsa = require ('../../lib/rsa'); -// eslint-disable-next-line max-lines-per-function, max-statements -describe ('crypto helper', () => { +// eslint-disable-next-line max-lines-per-function +describe ('signatures', () => { beforeEach (() => { jasmine.clock () .install (); @@ -25,95 +18,6 @@ describe ('crypto helper', () => { .uninstall (); }); - it ('random_hex', () => { - const hex = crypto.random_hex (16); - expect (hex.length) - .toEqual (16); - expect (hex) - .toMatch (/^[0-9a-f]+$/iu); - }); - - it ('random_hex with default length', () => { - const hex = crypto.random_hex (); - expect (hex.length) - .toEqual (8); - expect (hex) - .toMatch (/^[0-9a-f]+$/iu); - }); - - it ('random_hex should refuse length smaller 1', () => { - expect ( - () => (crypto.random_hex (0)) - ) - .toThrowError ('invalid length'); - }); - - it ('random_hex should always return correct length', () => { - for (let i = 1; i < 32; i++) { - const hex = crypto.random_hex (i); - expect (hex.length) - .toEqual (i); - } - }); - - it ('random_string', () => { - const str = crypto.random_string (16); - expect (str.length) - .toEqual (16); - }); - - it ('random_string with default length', () => { - const str = crypto.random_string (); - expect (str.length) - .toEqual (8); - }); - - it ('random_string should refuse length smaller 1', () => { - expect ( - () => (crypto.random_string (0)) - ) - .toThrowError ('invalid length'); - }); - - it ('random_string should always return correct length', () => { - for (let i = 1; i < 32; i++) { - const str = crypto.random_string (i); - expect (str.length) - .toEqual (i); - } - }); - - - it ('hash_sha512', () => { - const hash = crypto.hash_sha512 ('a', 'b'); - expect ( - hash - ) - .toEqual ( - // eslint-disable-next-line max-len - '2d408a0717ec188158278a796c689044361dc6fdde28d6f04973b80896e1823975cdbf12eb63f9e0591328ee235d80e9b5bf1aa6a44f4617ff3caf6400eb172d' - ); - }); - - it ('checksum', () => { - const hash = crypto.checksum ('foo'); - expect ( - hash - ) - .toEqual ( - // eslint-disable-next-line max-len - '2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae' - ); - }); - - it ('create_salt', () => { - const salt = crypto.create_salt (); - expect (salt.length) - .toEqual (64); - expect (salt) - .toMatch (/^[0-9a-f]+$/iu); - }); - it ('sign_object', () => { const obj = { foo: 'bar' }; expect (() => { @@ -123,6 +27,59 @@ describe ('crypto helper', () => { }).not.toThrow (); }); + it ('sign_object with rsa key', async () => { + const obj = { foo: 'bar' }; + const k = await rsa.generate_keypair (); + expect (() => { + const str = crypto.sign_object (obj, k.private_key); + expect (typeof str) + .toEqual ('string'); + }).not.toThrow (); + }); + + it ('verify_signature with rsa key', () => { + const obj = { foo: 'bar' }; + const str = 'U1GcsN3yZzSKxPH8jhCGTKiswfazB9rMfUtE5351LT11t6EmS7xfPjnt' + + '.5ytniC6q2ovoF7ZqbD8qk9r2kjjAcA9EYhLwC3wwJKPPsKdHSTFd7d9TzBP1skQ98X' + + 'LjRUkc2M8M84LmWLg76EvcY2pw6HwsFvCUoZYcKAJp3vkp9MQVrVYdHKMPkBjQKyy2V' + + 'KtEZiBsomBVJd6Hudd1YLMQ4J4s52iHsegDswKE9djYVEmgKkJUAiZJ2viFHw3fBbp2' + + 'Abo2Dm5oqYtw7Nn9RFstW3CcNQHV1PzHDKD56Uw3opuYwVZhQth8ux2CdkC2yMvgVsT' + + 'dUyCuu78ugaGvzsMXCbe2BzaPFDTE9JYtMcDFFP43nUGHNd6cWwzoKTZBX852Exz6Rb' + + 'VjcWUvL81dLPBLJV.2'; + const key = '-----BEGIN RSA PUBLIC KEY-----\n' + + 'MIIBCgKCAQEA4LCEoJYNwwksuzPESpmPziHp98WhY5Qml6RiN9uxrKGPV6QwwmDQ\n' + + 'ks6C+ZfYbFG9NCx1MEuWL0Tvp/6ZBhMyaJrI5iwo0CmSX3WdFcbXmdl0l6N1+5r7\n' + + 'l3SkKsr/AX4gwcDor4dYuLEv5KawGdfcP0IxsoAcIN1UJ5HJ+eheB3fVcSh/IIBf\n' + + 'O+cL/4Chw8eAaDBG5mZ1Xgd4gIjJGYAxgUNvaShGzs8k1y+jqjD5IkZ1h9dgoGJG\n' + + 'dUmjCLWrOzx8SqdqJYmQJX+6GNswnvVF30bkW+/MJZF/P2jLFtSa24Monh7axIqx\n' + + '8HG0xDw1Z98WV9oQh/vDP/KAs1cPp0AJlwIDAQAB\n' + + '-----END RSA PUBLIC KEY-----\n'; + const ver = crypto.verify_signature (str, key); + expect (ver) + .toEqual (obj); + }); + + it ('verify_signature reject with rsa key', () => { + const str = 'U1GcsN3yZzSKxPH8jhCGTKiswfazB9rMfUtE5351LT11t6EmS7xfPjnt' + + '.5ytniC6q2ovoF7ZqbD8qk9r2kjjAcA9EYhLwC3wwJKPPsKdHSTFd7d9TzBP1skQ98X' + + 'LjRUkc2M8M84LmWLg76EvcY2pw6HwsFvCUoZYcKAJp3vkp9MQVrVYdHKMPkBjQKyy2V' + + 'KtEZiBsomBVJd6Hudd1YLMQ4J4s52iHsegDswKE9djYVEmgKkJUAiZJ2viFHw3fBbp2' + + 'Abo2Dm5oqYtw7Nn9RFstW3CcNQHV1PzHDKD56Uw3opuYwVZhQth8ux2CdkC2yMvgVsT' + + 'dUyCuu78ugaGvzsMXCbe2BzaPFDTE9JYtMcDFFP43nUGHNd6cWwzoKTZBX852Exz6Rb' + + 'VjcWUvL81dLPBLJA.2'; + const key = '-----BEGIN RSA PUBLIC KEY-----\n' + + 'MIIBCgKCAQEA4LCEoJYNwwksuzPESpmPziHp98WhY5Qml6RiN9uxrKGPV6QwwmDQ\n' + + 'ks6C+ZfYbFG9NCx1MEuWL0Tvp/6ZBhMyaJrI5iwo0CmSX3WdFcbXmdl0l6N1+5r7\n' + + 'l3SkKsr/AX4gwcDor4dYuLEv5KawGdfcP0IxsoAcIN1UJ5HJ+eheB3fVcSh/IIBf\n' + + 'O+cL/4Chw8eAaDBG5mZ1Xgd4gIjJGYAxgUNvaShGzs8k1y+jqjD5IkZ1h9dgoGJG\n' + + 'dUmjCLWrOzx8SqdqJYmQJX+6GNswnvVF30bkW+/MJZF/P2jLFtSa24Monh7axIqx\n' + + '8HG0xDw1Z98WV9oQh/vDP/KAs1cPp0AJlwIDAQAB\n' + + '-----END RSA PUBLIC KEY-----\n'; + const ver = crypto.verify_signature (str, key); + expect (ver) + .toBeNull (); + }); + it ('should sign object with key info', () => { const obj = { foo: 'bar' }; expect (() => {