diff --git a/package-lock.json b/package-lock.json index 92dc81a22e..b1da52c3b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "@defich/jellyfish", + "name": "jellyfish", "lockfileVersion": 2, "requires": true, "packages": { @@ -13265,6 +13265,15 @@ "integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==", "dev": true }, + "node_modules/@types/randombytes": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/randombytes/-/randombytes-2.0.0.tgz", + "integrity": "sha512-bz8PhAVlwN72vqefzxa14DKNT8jK/mV66CSjwdVQM/k3Th3EPKfUtdMniwZgMedQTFuywAsfjnZsg+pEnltaMA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.0", "dev": true, @@ -14318,7 +14327,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "dev": true, "dependencies": { "buffer-xor": "^1.0.3", "cipher-base": "^1.0.0", @@ -14505,8 +14513,7 @@ "node_modules/buffer-xor": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", - "dev": true + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" }, "node_modules/builtin-status-codes": { "version": "3.0.0", @@ -17701,7 +17708,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "dev": true, "dependencies": { "md5.js": "^1.3.4", "safe-buffer": "^5.1.1" @@ -29370,8 +29376,10 @@ "dependencies": { "bech32": "^2.0.0", "bip66": "^1.1.5", + "browserify-aes": "^1.2.0", "bs58": "^4.0.1", "create-hash": "^1.2.0", + "randombytes": "^2.1.0", "tiny-secp256k1": "^1.1.6", "wif": "^2.0.6" }, @@ -29379,6 +29387,7 @@ "@types/bech32": "^1.1.2", "@types/bs58": "^4.0.1", "@types/create-hash": "^1.2.2", + "@types/randombytes": "^2.0.0", "@types/tiny-secp256k1": "^1.0.0", "@types/wif": "^2.0.2" } @@ -30673,12 +30682,15 @@ "@types/bech32": "^1.1.2", "@types/bs58": "^4.0.1", "@types/create-hash": "^1.2.2", + "@types/randombytes": "^2.0.0", "@types/tiny-secp256k1": "^1.0.0", "@types/wif": "^2.0.2", "bech32": "^2.0.0", "bip66": "^1.1.5", + "browserify-aes": "^1.2.0", "bs58": "^4.0.1", "create-hash": "^1.2.0", + "randombytes": "^2.1.0", "tiny-secp256k1": "^1.1.6", "wif": "^2.0.6" } @@ -40105,6 +40117,15 @@ "integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==", "dev": true }, + "@types/randombytes": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/randombytes/-/randombytes-2.0.0.tgz", + "integrity": "sha512-bz8PhAVlwN72vqefzxa14DKNT8jK/mV66CSjwdVQM/k3Th3EPKfUtdMniwZgMedQTFuywAsfjnZsg+pEnltaMA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/stack-utils": { "version": "2.0.0", "dev": true @@ -40842,7 +40863,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "dev": true, "requires": { "buffer-xor": "^1.0.3", "cipher-base": "^1.0.0", @@ -40982,8 +41002,7 @@ "buffer-xor": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", - "dev": true + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" }, "builtin-status-codes": { "version": "3.0.0", @@ -43331,7 +43350,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "dev": true, "requires": { "md5.js": "^1.3.4", "safe-buffer": "^5.1.1" diff --git a/packages/jellyfish-crypto/__tests__/aes256.test.ts b/packages/jellyfish-crypto/__tests__/aes256.test.ts new file mode 100644 index 0000000000..ef9f2a2f4f --- /dev/null +++ b/packages/jellyfish-crypto/__tests__/aes256.test.ts @@ -0,0 +1,63 @@ +import randomBytes from 'randombytes' +import { AES256 } from '../src' + +describe('Aes256', () => { + const raw = 'e9873d79c6d87dc0fb6a5778633389f4e93213303da61f20bd67fc233aa33262' + const privateKey = Buffer.from(raw, 'hex') + const passphrase = Buffer.from('password', 'ascii') + + let encrypted: Buffer + + it('encrypt', () => { + encrypted = AES256.encrypt(passphrase, privateKey) + expect(encrypted.length).toStrictEqual(48) // [16 bytes salt, 32 bytes cipher] + }) + + it('encrypt - should be able to overwrite cipher iv for better security measure', () => { + const iv = (): Buffer => Buffer.from('0102030405060708090a0b0c0d0e0f10', 'hex') + encrypted = AES256.encrypt(passphrase, privateKey, iv) + expect(encrypted.length).toStrictEqual(48) // [16 bytes salt, 32 bytes cipher] + }) + + it('encrypt - should reject non 16 bytes long iv', () => { + const iv = (): Buffer => Buffer.from('0102030405060708090a0b0c0d0e0f', 'hex') + expect(() => AES256.encrypt(passphrase, privateKey, iv)).toThrow('Initialization vector must be 16 bytes long') + }) + + it('decrypt - with valid passphrase', () => { + const decrypted = AES256.decrypt(passphrase, encrypted) + expect(decrypted.toString('hex')).toStrictEqual(raw) + }) + + it('decrypt - with invalid passphrase', () => { + const invalid = AES256.decrypt(passphrase.slice(1), encrypted) + expect(invalid.length).toStrictEqual(32) + expect(invalid.toString('hex')).not.toStrictEqual(raw) + }) + + it('decrypt - data too short, insufficient length to include salt', () => { + const invalidData = Buffer.alloc(16) + expect(() => { + AES256.decrypt(passphrase, invalidData) + }).toThrow('Provided "encrypted" must decrypt to a non-empty string or buffer') + }) +}) + +it('Repeat 1000 times with random data - should be consistent', () => { + for (let i = 0; i < 1000; i++) { + const sampleData = randomBytes(1000) + const passphrase = randomBytes(60) + + const encrypted = AES256.encrypt(passphrase, sampleData) + const decrypted = AES256.decrypt(passphrase, encrypted) + + const encryptedAgain = AES256.encrypt(passphrase, decrypted) + const decryptedAgain = AES256.decrypt(passphrase, encryptedAgain) + + // encrypted value are salted, it will be never the same + expect(encryptedAgain.toString('hex')).not.toStrictEqual(encrypted.toString('hex')) + // decrypted value are raw, recoverable using passphrase + expect(decryptedAgain.toString('hex')).toStrictEqual(decrypted.toString('hex')) + expect(decryptedAgain.toString('hex')).toStrictEqual(sampleData.toString('hex')) + } +}) diff --git a/packages/jellyfish-crypto/package.json b/packages/jellyfish-crypto/package.json index 8014e732d0..bf114b44ea 100644 --- a/packages/jellyfish-crypto/package.json +++ b/packages/jellyfish-crypto/package.json @@ -37,8 +37,10 @@ "dependencies": { "bech32": "^2.0.0", "bip66": "^1.1.5", + "browserify-aes": "^1.2.0", "bs58": "^4.0.1", "create-hash": "^1.2.0", + "randombytes": "^2.1.0", "tiny-secp256k1": "^1.1.6", "wif": "^2.0.6" }, @@ -46,6 +48,7 @@ "@types/bech32": "^1.1.2", "@types/bs58": "^4.0.1", "@types/create-hash": "^1.2.2", + "@types/randombytes": "^2.0.0", "@types/tiny-secp256k1": "^1.0.0", "@types/wif": "^2.0.2" } diff --git a/packages/jellyfish-crypto/src/aes256.ts b/packages/jellyfish-crypto/src/aes256.ts new file mode 100644 index 0000000000..31eb0dae8c --- /dev/null +++ b/packages/jellyfish-crypto/src/aes256.ts @@ -0,0 +1,54 @@ +import randomBytes from 'randombytes' +import aes from 'browserify-aes' +import { SHA256 } from './hash' + +const CIPHER_ALGORITHM = 'aes-256-ctr' + +/** + * Encrypt a clear-text message using AES-256 plus a random Initialization Vector. + * @see https://github.com/JamesMGreene/node-aes256 + * + * @param {Buffer} key A passphrase of any length to used to generate a symmetric session key. + * @param {Buffer} data The clear-text message or buffer to be encrypted. + * @param {(lengthOfBytes: number) => Buffer} rng Initialization vector generator, default using `crypto` or browserify `random-bytes` package + * @returns {Buffer} + */ +function encrypt (key: Buffer, data: Buffer, rng?: (lengthOfBytes: number) => Buffer): Buffer { + const sha256 = SHA256(key) + const initVector = rng === undefined ? randomBytes(16) : rng(16) + + if (initVector.length !== 16) { + throw new Error('Initialization vector must be 16 bytes long') + } + const cipher = aes.createCipheriv(CIPHER_ALGORITHM, sha256, initVector) + const ciphertext = cipher.update(data) + return Buffer.concat([initVector, ciphertext, cipher.final()]) +} + +/** + * Decrypt an encrypted message back to clear-text using AES-256 plus a random Initialization Vector. + * @see https://github.com/JamesMGreene/node-aes256 + * + * @param {Buffer} key A passphrase of any length to used to generate a symmetric session key. + * @param {Buffer} encrypted The encrypted message to be decrypted. + * @returns {Buffer} The original plain-text message or buffer. + */ +function decrypt (key: Buffer, encrypted: Buffer): Buffer { + if (encrypted.length < 17) { + throw new Error('Provided "encrypted" must decrypt to a non-empty string or buffer') + } + + const sha256 = SHA256(key) + const initVector = encrypted.slice(0, 16) + const decipher = aes.createDecipheriv(CIPHER_ALGORITHM, sha256, initVector) + + const ciphertext = encrypted.slice(16) + const deciphered = decipher.update(ciphertext) + const decipherFinal = decipher.final() + return Buffer.concat([deciphered, decipherFinal]) +} + +export const AES256 = { + encrypt, + decrypt +} diff --git a/packages/jellyfish-crypto/src/browserify-aes.d.ts b/packages/jellyfish-crypto/src/browserify-aes.d.ts new file mode 100644 index 0000000000..b251340588 --- /dev/null +++ b/packages/jellyfish-crypto/src/browserify-aes.d.ts @@ -0,0 +1,16 @@ +/** + * No DefinitelyTyped declarations found, declaring our own here. + * For ./aes256 package to work without crypto. + * @warning this is bare minimum, not fully typed as native crypto package. + */ + +declare module 'browserify-aes' { + interface Cipher { + update: (data: Buffer) => Buffer + final: () => Buffer + } + interface Decipher extends Cipher {} + + export function createCipheriv (algorithm: string, password: Buffer, iv: Buffer): Cipher + export function createDecipheriv (algorithm: string, password: Buffer, iv: Buffer): Decipher +} diff --git a/packages/jellyfish-crypto/src/index.ts b/packages/jellyfish-crypto/src/index.ts index bf916807d8..528b3ac1b8 100644 --- a/packages/jellyfish-crypto/src/index.ts +++ b/packages/jellyfish-crypto/src/index.ts @@ -1,3 +1,4 @@ +export * from './aes256' export * from './bech32' export * from './bs58' export * from './der'