Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

jellyfish-crypto: aes256 #331

Merged
merged 11 commits into from
Jun 11, 2021
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 43 additions & 0 deletions packages/jellyfish-crypto/__tests__/aes256.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { AES256 } from '../src'

const raw = 'e9873d79c6d87dc0fb6a5778633389f4e93213303da61f20bd67fc233aa33262'
const privateKey = Buffer.from(raw, 'hex')
const passphrase = Buffer.from('password', 'ascii')
ivan-zynesis marked this conversation as resolved.
Show resolved Hide resolved

describe('Aes256', () => {
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')
})
})
ivan-zynesis marked this conversation as resolved.
Show resolved Hide resolved
3 changes: 3 additions & 0 deletions packages/jellyfish-crypto/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,18 @@
"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"
},
"devDependencies": {
"@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"
}
Expand Down
58 changes: 58 additions & 0 deletions packages/jellyfish-crypto/src/aes256.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* Implementation reference: https://github.com/JamesMGreene/node-aes256
*/
ivan-zynesis marked this conversation as resolved.
Show resolved Hide resolved
import createHash from 'create-hash'
import randomBytes from 'randombytes'
import aes from 'browserify-aes'

const CIPHER_ALGORITHM = 'aes-256-ctr'

/**
* Encrypt a clear-text message using AES-256 plus a random Initialization Vector.
ivan-zynesis marked this conversation as resolved.
Show resolved Hide resolved
* @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 {() => Buffer} initVector Initialization vector provider, then to create AES cipher, default using `crypto` or browserify `random-bytes` package
ivan-zynesis marked this conversation as resolved.
Show resolved Hide resolved
* @returns {Buffer}
*/
function encrypt (key: Buffer, data: Buffer, initVector?: () => Buffer): Buffer {
ivan-zynesis marked this conversation as resolved.
Show resolved Hide resolved
const sha256 = createHash('sha256')
sha256.update(key)
ivan-zynesis marked this conversation as resolved.
Show resolved Hide resolved

const iv = initVector === undefined ? randomBytes(16) : initVector()

if (iv.length !== 16) {
throw new Error('Initialization vector must be 16 bytes long')
}
const cipher = aes.createCipheriv(CIPHER_ALGORITHM, sha256.digest(), iv)
const ciphertext = cipher.update(data)
return Buffer.concat([iv, ciphertext, cipher.final()])
}

/**
* Decrypt an encrypted message back to clear-text using AES-256 plus a random Initialization Vector.
* @param {Buffer} key A passphrase of any length to used to generate a symmetric session key.
ivan-zynesis marked this conversation as resolved.
Show resolved Hide resolved
* @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 = createHash('sha256')
sha256.update(key)
ivan-zynesis marked this conversation as resolved.
Show resolved Hide resolved

// Initialization Vector
const iv = encrypted.slice(0, 16)
ivan-zynesis marked this conversation as resolved.
Show resolved Hide resolved
const decipher = aes.createDecipheriv(CIPHER_ALGORITHM, sha256.digest(), iv)

const ciphertext = encrypted.slice(16)
const deciphered = decipher.update(ciphertext)
const decipherFinal = decipher.final()
return Buffer.concat([deciphered, decipherFinal])
}

export const AES256 = {
encrypt,
decrypt
}
16 changes: 16 additions & 0 deletions packages/jellyfish-crypto/src/browserify-aes.d.ts
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions packages/jellyfish-crypto/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './aes256'
export * from './bech32'
export * from './bs58'
export * from './der'
Expand Down