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

feat: aes encryption #59

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@
},
"./codecs/raw": {
"import": "./src/codecs/raw.js"
},
"./codecs/encrypted": {
"import": "./src/codecs/encrypted.js"
},
"./crypto/aes": {
"import": "./src/crypto/aes.js"
}
},
"devDependencies": {
Expand All @@ -103,6 +109,8 @@
"dependencies": {
"buffer": "^5.6.1",
"cids": "^1.0.2",
"js-crypto-aes": "^1.0.0",
"js-crypto-random": "^1.0.0",
"lodash.transform": "^4.6.0"
},
"directories": {
Expand Down
44 changes: 44 additions & 0 deletions src/codecs/encrypted.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// @ts-check
import * as varint from '../varint.js'
import { codec } from './codec.js'

const code = 0x1400

/**
* @template {number} Code
* @param {Object} options
* @param {Uint8Array} options.bytes
* @param {Uint8Array} options.iv
* @param {Code} options.code
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What' the code here ? I'm bit confused because as far as I understand 0x1400 code for encrypted codec, but then there is this other code which I'm not quite sure what that one represents.

Some comments would be really helpful here

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe it should be cipher instead of code, that's what it's for, a generic encrypted block that uses an iv with the cipher specified by looking up this integer in the multicodec table

* @returns {Uint8Array}
*/
const encode = ({ iv, code, bytes }) => {
const codeLength = varint.encodingLength(code)
const ivsizeLength = varint.encodingLength(iv.byteLength)
const length = codeLength + ivsizeLength + iv.byteLength + bytes.byteLength
const buff = new Uint8Array(length)
varint.encodeTo(code, buff)
let offset = codeLength
varint.encodeTo(iv.byteLength, buff, offset)
offset += ivsizeLength
buff.set(iv, offset)
offset += iv.byteLength
buff.set(bytes, offset)
return buff
}

/**
* @param {Uint8Array} bytes
*/
const decode = bytes => {
const [code, vlength] = varint.decode(bytes)
let offset = vlength
const [ivsize, ivsizeLength] = varint.decode(bytes.subarray(offset))
offset += ivsizeLength
const iv = bytes.subarray(offset, offset + ivsize)
offset += ivsize
bytes = bytes.slice(offset)
return { iv, code, bytes }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be great to give {iv, code, bytes} struct some name (and type def) that way type annotations for encode / decode are more straight forwards and it's also easier to conceptualize.

}

export default codec({ encode, decode, code, name: 'encrypted' })
62 changes: 62 additions & 0 deletions src/crypto/aes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import CID from '../cid.js'
import random from 'js-crypto-random'
import aes from 'js-crypto-aes'

/**
* @param {Uint8Array[]} buffers
*/
const concat = buffers => Uint8Array.from(buffers.map(b => [...b]).flat())

/**
* @template {'aes-gcm' | 'aes-cbc' | 'aes-ctr'} Name
* @template {number} Code
* @param {Object} options
* @param {Name} options.name
* @param {Code} options.code
* @param {number} options.ivsize
*/
const mkcrypto = ({ name, code, ivsize }) => {
// Line below does a type cast, because type checker can't infer that
// `toUpperCase` will result in desired string literal.
const cyperType = /** @type {import('js-crypto-aes/dist/params').cipherTypes} */(name.toUpperCase())
/**
* @param {Object} options
* @param {Uint8Array} options.key
* @param {Object} options.value
* @param {Uint8Array} options.value.bytes
* @param {Uint8Array} options.value.iv
*/
const decrypt = async ({ key, value: { iv, bytes } }) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It appears to me that it would make a lot of sense to give {iv, bytes, code} struct a name (and a type) so it's easier to conceptualize and talk about it.

I would also expect decrypt to expect value.code and assert it against the code instead of just ignoring it all together.

That way decrypt turns key+namedStruct into cid+bytes and encrypt turns key+cid-bytes into namedStruct.

Odd obsession with encrypt/decrypt symmetry also makes me wonder keys should be included in return types as well which would make output of encrypt input of decrypt and vice versa.

Copy link
Contributor Author

@mikeal mikeal Jan 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there’s more utility in having the implementation just use this destructing because then you don’t need to load the type in order to call the method.

If i know that I’m going to call the “decrypt AES function” I can call this with:

const iv = Buffer.from(something1)
const bytes = Buffer.from(something2)
const key = Buffer.from(something3)
decrypt({ key, value: { iv, bytes }})

But also, if I just have a block and I know that it’s encrypted I can spread it into the call.

const key = Buffer.from(something)
descrypt({ key, ...block })

This makes the decrypt function’s interface work with any block based decryption function. It may or may not have an iv, it may have other information in the value, cid, or bytes of the block it wants, and all of that will “just work” if I’m using block spreading.

bytes = await aes.decrypt(bytes, key, { name: cyperType, iv, tagLength: 16 })
const [cid, remainder] = CID.decodeFirst(bytes)
return { cid, bytes: remainder }
}
/**
* @param {Object} options
* @param {Uint8Array} options.key
* @param {Uint8Array} options.bytes
* @param {CID} options.cid
*/
const encrypt = async ({ key, cid, bytes }) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is yet another time where I wish CID+block bytes had a proper name to go by.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually find this to be a much better pattern because there are all kinds of ways that you may arrive at a CID + Bytes (car files reads, blocks, encrypted block decodes, etc) and destructuring them into only the parts these functions need is a great practice that gives the function maximal utility. Once you “name” it you end up with a type, and with blocks that tends to mean that you also have that codec and a decoded state, which is only useful when it is and is a burden when it’s not needed.

const iv = random.getRandomBytes(ivsize)
const msg = concat([cid.bytes, bytes])
bytes = await aes.encrypt(msg, key, { name: cyperType, iv, tagLength: 16 })
return { bytes, iv, code }
}

return {
code,
// Note: Do a type cast becasue `toLowerCase()` turns liternal type
// into a string.
name: /** @type {Name} */(name.toLowerCase()),
encrypt,
decrypt,
ivsize
}
}

const gcm = mkcrypto({ name: 'aes-gcm', code: 0x1401, ivsize: 12 })
const cbc = mkcrypto({ name: 'aes-cbc', code: 0x1402, ivsize: 16 })
const ctr = mkcrypto({ name: 'aes-ctr', code: 0x1403, ivsize: 12 })

export { gcm, cbc, ctr }
29 changes: 29 additions & 0 deletions test/test-block.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
/* globals describe, it */
import random from 'js-crypto-random'
import codec from 'multiformats/codecs/json'
import * as ciphers from 'multiformats/crypto/aes'
import encrypted from 'multiformats/codecs/encrypted'
import { sha256 as hasher } from 'multiformats/hashes/sha2'
import * as main from 'multiformats/block'
import { CID, bytes } from 'multiformats'
Expand Down Expand Up @@ -61,6 +64,32 @@ describe('block', () => {
})
})

describe('ciphers', () => {
const createTest = name => {
const json = codec
test(`aes-${name}`, async () => {
const block = await main.encode({ value: fixture, codec: json, hasher })
const crypto = ciphers[name]
const key = random.getRandomBytes(32)
const value = await crypto.encrypt({ ...block, key })
const eblock = await main.encode({ codec: encrypted, value, hasher })
const eeblock = await main.decode({ codec: encrypted, bytes: eblock.bytes, hasher })
same(eeblock.cid.toString(), eblock.cid.toString())
same([...eeblock.bytes], [...eblock.bytes])
same([...eeblock.value.bytes], [...eblock.value.bytes])
same([...eeblock.value.iv], [...eblock.value.iv])
const { cid, bytes } = await crypto.decrypt({ ...eblock, key })
same(cid.toString(), block.cid.toString())
same([...bytes], [...block.bytes])
const dblock = await main.create({ cid, bytes, codec: json, hasher })
same(dblock.value, block.value)
})
}
createTest('gcm')
createTest('cbc')
createTest('ctr')
})

test('kitchen sink', () => {
const sink = { one: { two: { arr: [true, false, null], three: 3, buff, link } } }
const block = main.createUnsafe({ value: sink, codec, bytes: true, cid: true })
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"strict": true,
"alwaysStrict": true,
"esModuleInterop": true,
"target": "ES2018",
"target": "ES2019",
"moduleResolution": "node",
"declaration": true,
"declarationMap": true,
Expand Down