From ac3011c873767d23061eb1cd00e88e3285b88132 Mon Sep 17 00:00:00 2001 From: Hayden Young Date: Tue, 21 May 2024 23:28:29 +0100 Subject: [PATCH] feat: Encrypt either data, payload or both. --- src/database.js | 6 ++- src/databases/documents.js | 4 +- src/databases/events.js | 13 ++++-- src/databases/keyvalue-indexed.js | 4 +- src/databases/keyvalue.js | 4 +- src/databases/utils/operation.js | 15 +++++++ src/databases/utils/payload.js | 15 +++++++ src/oplog/log.js | 10 +---- src/orbitdb.js | 4 +- test/orbitdb-encryption.test.js | 73 +++++++++++++++++++++++++++++++ 10 files changed, 125 insertions(+), 23 deletions(-) create mode 100644 src/databases/utils/operation.js create mode 100644 src/databases/utils/payload.js create mode 100644 test/orbitdb-encryption.test.js diff --git a/src/database.js b/src/database.js index 6496a5c..4bb0ab3 100644 --- a/src/database.js +++ b/src/database.js @@ -39,10 +39,12 @@ const defaultCacheSize = 1000 * automatically. Otherwise, false. * @param {function} [params.onUpdate] A function callback. Fired when an * entry is added to the oplog. + * @param {Function} options.encryptFn An encryption function. + * @param {Function} options.decryptFn A decryption function. * @return {module:Databases~Database} An instance of Database. * @instance */ -const Database = async ({ ipfs, identity, address, name, access, directory, meta, headsStorage, entryStorage, indexStorage, referencesCount, syncAutomatically, onUpdate }) => { +const Database = async ({ ipfs, identity, address, name, access, directory, meta, headsStorage, entryStorage, indexStorage, referencesCount, syncAutomatically, onUpdate, encryptFn, decryptFn }) => { /** * @namespace module:Databases~Database * @description The instance returned by {@link module:Database~Database}. @@ -108,7 +110,7 @@ const Database = async ({ ipfs, identity, address, name, access, directory, meta await LevelStorage({ path: pathJoin(directory, '/log/_index/') }) ) - const log = await Log(identity, { logId: address, access, entryStorage, headsStorage, indexStorage }) + const log = await Log(identity, { logId: address, access, entryStorage, headsStorage, indexStorage, encryptFn, decryptFn }) const events = new EventEmitter() diff --git a/src/databases/documents.js b/src/databases/documents.js index e89d3fc..a0faca1 100644 --- a/src/databases/documents.js +++ b/src/databases/documents.js @@ -25,8 +25,8 @@ const DefaultOptions = { indexBy: '_id' } * @return {module:Databases.Databases-Documents} A Documents function. * @memberof module:Databases */ -const Documents = ({ indexBy } = DefaultOptions) => async ({ ipfs, identity, address, name, access, directory, meta, headsStorage, entryStorage, indexStorage, referencesCount, syncAutomatically, onUpdate }) => { - const database = await Database({ ipfs, identity, address, name, access, directory, meta, headsStorage, entryStorage, indexStorage, referencesCount, syncAutomatically }) +const Documents = ({ indexBy } = DefaultOptions) => async ({ ipfs, identity, address, name, access, directory, meta, headsStorage, entryStorage, indexStorage, referencesCount, syncAutomatically, onUpdate, encryptFn, decryptFn }) => { + const database = await Database({ ipfs, identity, address, name, access, directory, meta, headsStorage, entryStorage, indexStorage, referencesCount, syncAutomatically, encryptFn, decryptFn }) const { addOperation, log } = database diff --git a/src/databases/events.js b/src/databases/events.js index 1ddfe87..3093723 100644 --- a/src/databases/events.js +++ b/src/databases/events.js @@ -7,6 +7,8 @@ * @augments module:Databases~Database */ import Database from '../database.js' +import { Operation } from './utils/operation.js' +import { Payload } from './utils/payload.js' const type = 'events' @@ -15,8 +17,8 @@ const type = 'events' * @return {module:Databases.Databases-Events} A Events function. * @memberof module:Databases */ -const Events = () => async ({ ipfs, identity, address, name, access, directory, meta, headsStorage, entryStorage, indexStorage, referencesCount, syncAutomatically, onUpdate }) => { - const database = await Database({ ipfs, identity, address, name, access, directory, meta, headsStorage, entryStorage, indexStorage, referencesCount, syncAutomatically, onUpdate }) +const Events = () => async ({ ipfs, identity, address, name, access, directory, meta, headsStorage, entryStorage, indexStorage, referencesCount, syncAutomatically, onUpdate, encryptFn, decryptFn }) => { + const database = await Database({ ipfs, identity, address, name, access, directory, meta, headsStorage, entryStorage, indexStorage, referencesCount, syncAutomatically, onUpdate, encryptFn, decryptFn }) const { addOperation, log } = database @@ -29,7 +31,8 @@ const Events = () => async ({ ipfs, identity, address, name, access, directory, * @instance */ const add = async (value) => { - return addOperation({ op: 'ADD', key: null, value }) + const op = await Operation('ADD', null, value, { encryptFn, encryptValue: true, encryptOp: false }) + return addOperation(op) } /** @@ -42,7 +45,9 @@ const Events = () => async ({ ipfs, identity, address, name, access, directory, */ const get = async (hash) => { const entry = await log.get(hash) - return entry.payload.value + const { value } = await Payload(entry.payload, { decryptFn, decryptValue: true, decryptOp: false }) + + return value } /** diff --git a/src/databases/keyvalue-indexed.js b/src/databases/keyvalue-indexed.js index ceff536..97eb16c 100644 --- a/src/databases/keyvalue-indexed.js +++ b/src/databases/keyvalue-indexed.js @@ -109,7 +109,7 @@ const Index = ({ directory } = {}) => async () => { * function. * @memberof module:Databases */ -const KeyValueIndexed = () => async ({ ipfs, identity, address, name, access, directory, meta, headsStorage, entryStorage, indexStorage, referencesCount, syncAutomatically, onUpdate }) => { +const KeyValueIndexed = () => async ({ ipfs, identity, address, name, access, directory, meta, headsStorage, entryStorage, indexStorage, referencesCount, syncAutomatically, onUpdate, encryptFn, decryptFn }) => { // Set up the directory for an index directory = pathJoin(directory || './orbitdb', `./${address}/_index/`) @@ -117,7 +117,7 @@ const KeyValueIndexed = () => async ({ ipfs, identity, address, name, access, di const index = await Index({ directory })() // Set up the underlying KeyValue database - const keyValueStore = await KeyValue()({ ipfs, identity, address, name, access, directory, meta, headsStorage, entryStorage, indexStorage, referencesCount, syncAutomatically, onUpdate: index.update }) + const keyValueStore = await KeyValue()({ ipfs, identity, address, name, access, directory, meta, headsStorage, entryStorage, indexStorage, referencesCount, syncAutomatically, onUpdate: index.update, encryptFn, decryptFn }) /** * Gets a value from the store by key. diff --git a/src/databases/keyvalue.js b/src/databases/keyvalue.js index 0e2bc62..4488f52 100644 --- a/src/databases/keyvalue.js +++ b/src/databases/keyvalue.js @@ -15,8 +15,8 @@ const type = 'keyvalue' * @return {module:Databases.Databases-KeyValue} A KeyValue function. * @memberof module:Databases */ -const KeyValue = () => async ({ ipfs, identity, address, name, access, directory, meta, headsStorage, entryStorage, indexStorage, referencesCount, syncAutomatically, onUpdate }) => { - const database = await Database({ ipfs, identity, address, name, access, directory, meta, headsStorage, entryStorage, indexStorage, referencesCount, syncAutomatically, onUpdate }) +const KeyValue = () => async ({ ipfs, identity, address, name, access, directory, meta, headsStorage, entryStorage, indexStorage, referencesCount, syncAutomatically, onUpdate, encryptFn, decryptFn }) => { + const database = await Database({ ipfs, identity, address, name, access, directory, meta, headsStorage, entryStorage, indexStorage, referencesCount, syncAutomatically, onUpdate, encryptFn, decryptFn }) const { addOperation, log } = database diff --git a/src/databases/utils/operation.js b/src/databases/utils/operation.js new file mode 100644 index 0000000..03a4863 --- /dev/null +++ b/src/databases/utils/operation.js @@ -0,0 +1,15 @@ +export const Operation = async (op, key, value, { encryptFn, encryptValue, encryptOp } = {}) => { + let operation = { op, key, value } + + if (encryptFn) { + if (encryptValue) { + operation.value = await encryptFn(value) + } + + if (encryptOp) { + operation = await encryptFn(JSON.stringify(operation)) + } + } + + return operation +} diff --git a/src/databases/utils/payload.js b/src/databases/utils/payload.js new file mode 100644 index 0000000..050c876 --- /dev/null +++ b/src/databases/utils/payload.js @@ -0,0 +1,15 @@ +export const Payload = async (payload, { decryptFn, decryptValue, decryptOp }) => { + if (decryptFn) { + if (decryptOp) { + payload = JSON.parse(await decryptFn(payload)) + } + + if (decryptValue) { + payload.value = await decryptFn(payload.value) + } + } + + const { op, key, value } = payload + + return { op, key, value } +} diff --git a/src/oplog/log.js b/src/oplog/log.js index 7edf6f5..fc6e819 100644 --- a/src/oplog/log.js +++ b/src/oplog/log.js @@ -56,7 +56,7 @@ const DefaultAccessController = async () => { * @memberof module:Log * @instance */ -const Log = async (identity, { logId, logHeads, access, entryStorage, headsStorage, indexStorage, sortFn, encryptFn, decryptFn } = {}) => { +const Log = async (identity, { logId, logHeads, access, entryStorage, headsStorage, indexStorage, sortFn } = {}) => { /** * @namespace Log * @description The instance returned by {@link module:Log} @@ -139,10 +139,6 @@ const Log = async (identity, { logId, logHeads, access, entryStorage, headsStora if (bytes) { const entry = await Entry.decode(bytes) - if (decryptFn) { - entry.payload = await decryptFn(entry.payload) - } - return entry } } @@ -176,10 +172,6 @@ const Log = async (identity, { logId, logHeads, access, entryStorage, headsStora // (skips the heads which are covered by the next field) const refs = await getReferences(heads_, options.referencesCount + heads_.length) - if (encryptFn) { - data = await encryptFn(data) - } - // Create the entry const entry = await Entry.create( identity, diff --git a/src/orbitdb.js b/src/orbitdb.js index 2ebc847..f2eb6f9 100644 --- a/src/orbitdb.js +++ b/src/orbitdb.js @@ -112,7 +112,7 @@ const OrbitDB = async ({ ipfs, id, identity, identities, directory } = {}) => { * @instance * @async */ - const open = async (address, { type, meta, sync, Database, AccessController, headsStorage, entryStorage, indexStorage, referencesCount } = {}) => { + const open = async (address, { type, meta, sync, Database, AccessController, headsStorage, entryStorage, indexStorage, referencesCount, encryptFn, decryptFn } = {}) => { let name, manifest, accessController if (databases[address]) { @@ -153,7 +153,7 @@ const OrbitDB = async ({ ipfs, id, identity, identities, directory } = {}) => { address = address.toString() - const db = await Database({ ipfs, identity, address, name, access: accessController, directory, meta, syncAutomatically: sync, headsStorage, entryStorage, indexStorage, referencesCount }) + const db = await Database({ ipfs, identity, address, name, access: accessController, directory, meta, syncAutomatically: sync, headsStorage, entryStorage, indexStorage, referencesCount, encryptFn, decryptFn }) db.events.on('close', onDatabaseClosed(address)) diff --git a/test/orbitdb-encryption.test.js b/test/orbitdb-encryption.test.js new file mode 100644 index 0000000..c214c31 --- /dev/null +++ b/test/orbitdb-encryption.test.js @@ -0,0 +1,73 @@ +import { strictEqual } from 'assert' +import { rimraf } from 'rimraf' +import path from 'path' +import OrbitDB from '../src/orbitdb.js' +// import waitFor from './utils/wait-for.js' +import connectPeers from './utils/connect-nodes.js' +// import IPFSAccessController from '../src/access-controllers/ipfs.js' +// import OrbitDBAccessController from '../src/access-controllers/orbitdb.js' +import createHelia from './utils/create-helia.js' +import { encrypt, decrypt } from './utils/encrypt.js' + +const dbPath = './orbitdb/tests/write-permissions' + +describe('Encryption/Decryption', function () { + this.timeout(20000) + + let ipfs1, ipfs2 + let orbitdb1, orbitdb2 + let db1 /*, db2 */ + + before(async () => { + [ipfs1, ipfs2] = await Promise.all([createHelia(), createHelia()]) + await connectPeers(ipfs1, ipfs2) + + orbitdb1 = await OrbitDB({ ipfs: ipfs1, id: 'user1', directory: path.join(dbPath, '1') }) + orbitdb2 = await OrbitDB({ ipfs: ipfs2, id: 'user2', directory: path.join(dbPath, '2') }) + }) + + after(async () => { + if (orbitdb1) { + await orbitdb1.stop() + } + + if (orbitdb2) { + await orbitdb2.stop() + } + + if (ipfs1) { + await ipfs1.stop() + } + + if (ipfs2) { + await ipfs2.stop() + } + + await rimraf('./orbitdb') + await rimraf('./ipfs1') + await rimraf('./ipfs2') + }) + + afterEach(async () => { + await db1.drop() + await db1.close() + + // await db2.drop() + // await db2.close() + }) + + it('can encrypt/decrypt data', async () => { + const keystore = orbitdb1.keystore + const keys = await keystore.createKey('encryption-test') + + const privateKey = await keystore.getKey('encryption-test') + const publicKey = await keystore.getPublic(keys) + + const encryptFn = encrypt({ publicKey }) + const decryptFn = decrypt({ privateKey }) + db1 = await orbitdb1.open('encryption-test-1', { encryptFn, decryptFn }) + + const hash = await db1.add('record 1') + strictEqual(await db1.get(hash), 'record 1') + }) +})