Merge pull request #1187 from orbitdb/feat/encryption-entries

Feat/encryption entries
This commit is contained in:
Hayden Young 2024-07-14 19:56:34 +08:00 committed by GitHub
commit c173a01389
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 829 additions and 515 deletions

View File

@ -45,8 +45,10 @@ EventEmitter.defaultMaxListeners = 10000
let connected = false
const onJoin = async (peerId) => (connected = true)
const onError = async (err) => console.error(err)
db2.events.on('join', onJoin)
db2.events.on('error', onError)
await waitFor(() => connected, () => true)

View File

@ -38,7 +38,7 @@ export default (env, argv) => {
],
fallback: {
path: require.resolve('path-browserify'),
crypto: require.resolve('crypto-browserify'),
crypto: false,
stream: require.resolve('stream-browserify'),
process: false
}

280
package-lock.json generated
View File

@ -28,7 +28,6 @@
"blockstore-level": "^1.1.7",
"c8": "^8.0.1",
"cross-env": "^7.0.3",
"crypto-browserify": "^3.12.0",
"fs-extra": "^11.2.0",
"helia": "^4.0.1",
"it-all": "^3.0.4",
@ -7626,17 +7625,6 @@
"dev": true,
"peer": true
},
"node_modules/asn1.js": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
"integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==",
"dev": true,
"dependencies": {
"bn.js": "^4.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0"
}
},
"node_modules/asn1js": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.5.tgz",
@ -7984,148 +7972,6 @@
"safe-buffer": "^5.0.1"
}
},
"node_modules/browserify-cipher": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz",
"integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==",
"dev": true,
"dependencies": {
"browserify-aes": "^1.0.4",
"browserify-des": "^1.0.0",
"evp_bytestokey": "^1.0.0"
}
},
"node_modules/browserify-des": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz",
"integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==",
"dev": true,
"dependencies": {
"cipher-base": "^1.0.1",
"des.js": "^1.0.0",
"inherits": "^2.0.1",
"safe-buffer": "^5.1.2"
}
},
"node_modules/browserify-rsa": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz",
"integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==",
"dev": true,
"dependencies": {
"bn.js": "^5.0.0",
"randombytes": "^2.0.1"
}
},
"node_modules/browserify-rsa/node_modules/bn.js": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz",
"integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==",
"dev": true
},
"node_modules/browserify-sign": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.3.tgz",
"integrity": "sha512-JWCZW6SKhfhjJxO8Tyiiy+XYB7cqd2S5/+WeYHsKdNKFlCBhKbblba1A/HN/90YwtxKc8tCErjffZl++UNmGiw==",
"dev": true,
"dependencies": {
"bn.js": "^5.2.1",
"browserify-rsa": "^4.1.0",
"create-hash": "^1.2.0",
"create-hmac": "^1.1.7",
"elliptic": "^6.5.5",
"hash-base": "~3.0",
"inherits": "^2.0.4",
"parse-asn1": "^5.1.7",
"readable-stream": "^2.3.8",
"safe-buffer": "^5.2.1"
},
"engines": {
"node": ">= 0.12"
}
},
"node_modules/browserify-sign/node_modules/bn.js": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz",
"integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==",
"dev": true
},
"node_modules/browserify-sign/node_modules/elliptic": {
"version": "6.5.5",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.5.tgz",
"integrity": "sha512-7EjbcmUm17NQFu4Pmgmq2olYMj8nwMnpcddByChSUjArp8F5DQWcIcpriwO4ZToLNAJig0yiyjswfyGNje/ixw==",
"dev": true,
"dependencies": {
"bn.js": "^4.11.9",
"brorand": "^1.1.0",
"hash.js": "^1.0.0",
"hmac-drbg": "^1.0.1",
"inherits": "^2.0.4",
"minimalistic-assert": "^1.0.1",
"minimalistic-crypto-utils": "^1.0.1"
}
},
"node_modules/browserify-sign/node_modules/elliptic/node_modules/bn.js": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
"dev": true
},
"node_modules/browserify-sign/node_modules/hash-base": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz",
"integrity": "sha512-EeeoJKjTyt868liAlVmcv2ZsUfGHlE3Q+BICOXcZiwN3osr5Q/zFGYmTJpoIzuaSTAwndFy+GqhEwlU4L3j4Ow==",
"dev": true,
"dependencies": {
"inherits": "^2.0.1",
"safe-buffer": "^5.0.1"
},
"engines": {
"node": ">=4"
}
},
"node_modules/browserify-sign/node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"dev": true
},
"node_modules/browserify-sign/node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dev": true,
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/browserify-sign/node_modules/readable-stream/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true
},
"node_modules/browserify-sign/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/browserify-sign/node_modules/string_decoder/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true
},
"node_modules/browserslist": {
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
@ -8886,7 +8732,8 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"dev": true
"dev": true,
"peer": true
},
"node_modules/cosmiconfig": {
"version": "5.2.1",
@ -9014,16 +8861,6 @@
"node": ">=0.8"
}
},
"node_modules/create-ecdh": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz",
"integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==",
"dev": true,
"dependencies": {
"bn.js": "^4.1.0",
"elliptic": "^6.5.3"
}
},
"node_modules/create-hash": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
@ -9081,28 +8918,6 @@
"node": ">= 8"
}
},
"node_modules/crypto-browserify": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
"integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==",
"dev": true,
"dependencies": {
"browserify-cipher": "^1.0.0",
"browserify-sign": "^4.0.0",
"create-ecdh": "^4.0.0",
"create-hash": "^1.1.0",
"create-hmac": "^1.1.0",
"diffie-hellman": "^5.0.0",
"inherits": "^2.0.1",
"pbkdf2": "^3.0.3",
"public-encrypt": "^4.0.0",
"randombytes": "^2.0.0",
"randomfill": "^1.0.3"
},
"engines": {
"node": "*"
}
},
"node_modules/crypto-random-string": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz",
@ -9335,16 +9150,6 @@
"node": ">=18"
}
},
"node_modules/des.js": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz",
"integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==",
"dev": true,
"dependencies": {
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0"
}
},
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
@ -9380,17 +9185,6 @@
"node": ">=0.3.1"
}
},
"node_modules/diffie-hellman": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
"integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
"dev": true,
"dependencies": {
"bn.js": "^4.1.0",
"miller-rabin": "^4.0.0",
"randombytes": "^2.0.0"
}
},
"node_modules/dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@ -14388,19 +14182,6 @@
"node": ">=8.6"
}
},
"node_modules/miller-rabin": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz",
"integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==",
"dev": true,
"dependencies": {
"bn.js": "^4.0.0",
"brorand": "^1.0.1"
},
"bin": {
"miller-rabin": "bin/miller-rabin"
}
},
"node_modules/mime": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
@ -15522,36 +15303,6 @@
"node": ">=6"
}
},
"node_modules/parse-asn1": {
"version": "5.1.7",
"resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.7.tgz",
"integrity": "sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg==",
"dev": true,
"dependencies": {
"asn1.js": "^4.10.1",
"browserify-aes": "^1.2.0",
"evp_bytestokey": "^1.0.3",
"hash-base": "~3.0",
"pbkdf2": "^3.1.2",
"safe-buffer": "^5.2.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/parse-asn1/node_modules/hash-base": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz",
"integrity": "sha512-EeeoJKjTyt868liAlVmcv2ZsUfGHlE3Q+BICOXcZiwN3osr5Q/zFGYmTJpoIzuaSTAwndFy+GqhEwlU4L3j4Ow==",
"dev": true,
"dependencies": {
"inherits": "^2.0.1",
"safe-buffer": "^5.0.1"
},
"engines": {
"node": ">=4"
}
},
"node_modules/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
@ -16106,7 +15857,8 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"dev": true
"dev": true,
"peer": true
},
"node_modules/progress-events": {
"version": "1.0.0",
@ -16172,20 +15924,6 @@
"uint8arrays": "^5.0.1"
}
},
"node_modules/public-encrypt": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz",
"integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==",
"dev": true,
"dependencies": {
"bn.js": "^4.1.0",
"browserify-rsa": "^4.0.0",
"create-hash": "^1.1.0",
"parse-asn1": "^5.0.0",
"randombytes": "^2.0.1",
"safe-buffer": "^5.1.2"
}
},
"node_modules/pump": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
@ -16272,16 +16010,6 @@
"safe-buffer": "^5.1.0"
}
},
"node_modules/randomfill": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz",
"integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==",
"dev": true,
"dependencies": {
"randombytes": "^2.0.5",
"safe-buffer": "^5.1.0"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",

View File

@ -37,7 +37,6 @@
"blockstore-level": "^1.1.7",
"c8": "^8.0.1",
"cross-env": "^7.0.3",
"crypto-browserify": "^3.12.0",
"fs-extra": "^11.2.0",
"helia": "^4.0.1",
"it-all": "^3.0.4",

View File

@ -7,7 +7,7 @@
import { EventEmitter } from 'events'
import PQueue from 'p-queue'
import Sync from './sync.js'
import { Log, Entry } from './oplog/index.js'
import { Log } from './oplog/index.js'
import { ComposedStorage, LRUStorage, IPFSBlockStorage, LevelStorage } from './storage/index.js'
import pathJoin from './utils/path-join.js'
@ -110,6 +110,8 @@ const Database = async ({ ipfs, identity, address, name, access, directory, meta
await LevelStorage({ path: pathJoin(directory, '/log/_index/') })
)
encryption = encryption || {}
const log = await Log(identity, { logId: address, access, entryStorage, headsStorage, indexStorage, encryption })
const events = new EventEmitter()
@ -140,17 +142,20 @@ const Database = async ({ ipfs, identity, address, name, access, directory, meta
return hash
}
const applyOperation = async (bytes) => {
const applyOperation = async (entry) => {
const task = async () => {
const entry = await Entry.decode(bytes)
if (entry) {
const updated = await log.joinEntry(entry)
if (updated) {
if (onUpdate) {
await onUpdate(log, entry)
try {
if (entry) {
const updated = await log.joinEntry(entry)
if (updated) {
if (onUpdate) {
await onUpdate(log, entry)
}
events.emit('update', entry)
}
events.emit('update', entry)
}
} catch (e) {
console.error(e)
}
}
await queue.add(task)

View File

@ -0,0 +1,117 @@
/*
Source:
https://github.com/libp2p/js-libp2p/blob/0b55625d146940994a306101650a55ee58e32f6c/packages/crypto/src/ciphers/aes-gcm.browser.ts
More information:
- https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto
- https://github.com/bradyjoslin/webcrypto-example/blob/master/script.js
- https://github.com/mdn/dom-examples/blob/main/web-crypto/encrypt-decrypt/aes-gcm.js
*/
import crypto from 'crypto'
import { concat } from 'uint8arrays/concat'
import { fromString } from 'uint8arrays/from-string'
// Polyfill fix for browsers
const getCrypto = () => {
if (typeof global.crypto !== 'undefined') {
return global.crypto
} else {
return crypto
}
}
// WebKit on Linux does not support deriving a key from an empty PBKDF2 key.
// So, as a workaround, we provide the generated key as a constant. We test that
// this generated key is accurate in test/workaround.spec.ts
// Generated via:
// await crypto.subtle.exportKey('jwk',
// await crypto.subtle.deriveKey(
// { name: 'PBKDF2', salt: new Uint8Array(16), iterations: 32767, hash: { name: 'SHA-256' } },
// await crypto.subtle.importKey('raw', new Uint8Array(0), { name: 'PBKDF2' }, false, ['deriveKey']),
// { name: 'AES-GCM', length: 128 }, true, ['encrypt', 'decrypt'])
// )
export const derivedEmptyPasswordKey = { alg: 'A128GCM', ext: true, k: 'scm9jmO_4BJAgdwWGVulLg', key_ops: ['encrypt', 'decrypt'], kty: 'oct' }
// Based off of code from https://github.com/luke-park/SecureCompatibleEncryptionExamples
export function AES (opts) {
const algorithm = opts?.algorithm ?? 'AES-GCM'
let keyLength = opts?.keyLength ?? 16
const nonceLength = opts?.nonceLength ?? 12
const digest = opts?.digest ?? 'SHA-256'
const saltLength = opts?.saltLength ?? 16
const iterations = opts?.iterations ?? 32767
// const crypto = webcrypto.get();
const crypto = getCrypto()
keyLength *= 8 // Browser crypto uses bits instead of bytes
/**
* Uses the provided password to derive a pbkdf2 key. The key
* will then be used to encrypt the data.
*/
async function encrypt (data, password) {
const salt = crypto.getRandomValues(new Uint8Array(saltLength))
const nonce = crypto.getRandomValues(new Uint8Array(nonceLength))
const aesGcm = { name: algorithm, iv: nonce }
if (typeof password === 'string') {
password = fromString(password)
}
let cryptoKey
if (password.length === 0) {
cryptoKey = await crypto.subtle.importKey('jwk', derivedEmptyPasswordKey, { name: 'AES-GCM' }, true, ['encrypt'])
try {
const deriveParams = { name: 'PBKDF2', salt, iterations, hash: { name: digest } }
const runtimeDerivedEmptyPassword = await crypto.subtle.importKey('raw', password, { name: 'PBKDF2' }, false, ['deriveKey'])
cryptoKey = await crypto.subtle.deriveKey(deriveParams, runtimeDerivedEmptyPassword, { name: algorithm, length: keyLength }, true, ['encrypt'])
} catch {
cryptoKey = await crypto.subtle.importKey('jwk', derivedEmptyPasswordKey, { name: 'AES-GCM' }, true, ['encrypt'])
}
} else {
// Derive a key using PBKDF2.
const deriveParams = { name: 'PBKDF2', salt, iterations, hash: { name: digest } }
const rawKey = await crypto.subtle.importKey('raw', password, { name: 'PBKDF2' }, false, ['deriveKey'])
cryptoKey = await crypto.subtle.deriveKey(deriveParams, rawKey, { name: algorithm, length: keyLength }, true, ['encrypt'])
}
// Encrypt the string.
const ciphertext = await crypto.subtle.encrypt(aesGcm, cryptoKey, data)
return concat([salt, aesGcm.iv, new Uint8Array(ciphertext)])
}
/**
* Uses the provided password to derive a pbkdf2 key. The key
* will then be used to decrypt the data. The options used to create
* this decryption cipher must be the same as those used to create
* the encryption cipher.
*/
async function decrypt (data, password) {
const salt = data.subarray(0, saltLength)
const nonce = data.subarray(saltLength, saltLength + nonceLength)
const ciphertext = data.subarray(saltLength + nonceLength)
const aesGcm = { name: algorithm, iv: nonce }
if (typeof password === 'string') {
password = fromString(password)
}
let cryptoKey
if (password.length === 0) {
try {
const deriveParams = { name: 'PBKDF2', salt, iterations, hash: { name: digest } }
const runtimeDerivedEmptyPassword = await crypto.subtle.importKey('raw', password, { name: 'PBKDF2' }, false, ['deriveKey'])
cryptoKey = await crypto.subtle.deriveKey(deriveParams, runtimeDerivedEmptyPassword, { name: algorithm, length: keyLength }, true, ['decrypt'])
} catch {
cryptoKey = await crypto.subtle.importKey('jwk', derivedEmptyPasswordKey, { name: 'AES-GCM' }, true, ['decrypt'])
}
} else {
// Derive the key using PBKDF2.
const deriveParams = { name: 'PBKDF2', salt, iterations, hash: { name: digest } }
const rawKey = await crypto.subtle.importKey('raw', password, { name: 'PBKDF2' }, false, ['deriveKey'])
cryptoKey = await crypto.subtle.deriveKey(deriveParams, rawKey, { name: algorithm, length: keyLength }, true, ['decrypt'])
}
// Decrypt the string.
const plaintext = await crypto.subtle.decrypt(aesGcm, cryptoKey, ciphertext)
return new Uint8Array(plaintext)
}
const cipher = {
encrypt,
decrypt
}
return cipher
}

6
src/encryption/index.js Normal file
View File

@ -0,0 +1,6 @@
/**
* @module Encryption
* @description
* Encryption modules for OrbitDB.
*/
export { default as PasswordEncryption } from './password.js'

View File

@ -0,0 +1,29 @@
/**
* @namespace Encryption-Password
* @memberof module:Encryption
* @description
* Password encryption module encrypts data using AES-GCM PBKDF2.
*/
import { AES } from './aes-gcm-pbkdf2.js'
const PasswordEncryption = async ({ password, aesOptions }) => {
aesOptions = aesOptions || {}
const aes = AES(aesOptions)
const encrypt = (value) => {
return aes.encrypt(value, password)
}
const decrypt = (value) => {
return aes.decrypt(value, password)
}
return {
encrypt,
decrypt
}
}
export default PasswordEncryption

View File

@ -41,3 +41,7 @@ export {
MemoryStorage,
ComposedStorage
} from './storage/index.js'
export {
PasswordEncryption
} from './encryption/index.js'

View File

@ -55,7 +55,7 @@ const hashStringEncoding = base58btc
* // { payload: "hello", next: [], ... }
* @private
*/
const create = async (identity, id, payload, clock = null, next = [], refs = []) => {
const create = async (identity, id, payload, encryptPayloadFn, clock = null, next = [], refs = []) => {
if (identity == null) throw new Error('Identity is required, cannot create entry')
if (id == null) throw new Error('Entry requires an id')
if (payload == null) throw new Error('Entry requires a payload')
@ -63,9 +63,16 @@ const create = async (identity, id, payload, clock = null, next = [], refs = [])
clock = clock || Clock(identity.publicKey)
let encryptedPayload
if (encryptPayloadFn) {
const { bytes: encodedPayloadBytes } = await Block.encode({ value: payload, codec, hasher })
encryptedPayload = await encryptPayloadFn(encodedPayloadBytes)
}
const entry = {
id, // For determining a unique chain
payload, // Can be any dag-cbor encodeable data
payload: encryptedPayload || payload, // Can be any dag-cbor encodeable data
next, // Array of strings of CIDs
refs, // Array of strings of CIDs
clock, // Clock
@ -78,8 +85,13 @@ const create = async (identity, id, payload, clock = null, next = [], refs = [])
entry.key = identity.publicKey
entry.identity = identity.hash
entry.sig = signature
entry.payload = payload
return encode(entry)
if (encryptPayloadFn) {
entry._payload = encryptedPayload
}
return entry
}
/**
@ -97,13 +109,15 @@ const verify = async (identities, entry) => {
if (!entry.key) throw new Error("Entry doesn't have a key")
if (!entry.sig) throw new Error("Entry doesn't have a signature")
const e = Object.assign({}, entry)
const value = {
id: entry.id,
payload: entry.payload,
next: entry.next,
refs: entry.refs,
clock: entry.clock,
v: entry.v
id: e.id,
payload: e._payload || e.payload,
next: e.next,
refs: e.refs,
clock: e.clock,
v: e.v
}
const { bytes } = await Block.encode({ value, codec, hasher })
@ -136,7 +150,7 @@ const isEntry = (obj) => {
* @private
*/
const isEqual = (a, b) => {
return a && b && a.hash === b.hash
return a && b && a.hash && a.hash === b.hash
}
/**
@ -146,13 +160,39 @@ const isEqual = (a, b) => {
* @memberof module:Log~Entry
* @private
*/
const decode = async (bytes) => {
const { cid, value } = await Block.decode({ bytes, codec, hasher })
const decode = async (bytes, decryptEntryFn, decryptPayloadFn) => {
let cid
if (decryptEntryFn) {
try {
const encryptedEntry = await Block.decode({ bytes, codec, hasher })
bytes = await decryptEntryFn(encryptedEntry.value)
cid = encryptedEntry.cid
} catch (e) {
throw new Error('Could not decrypt entry')
}
}
const decodedEntry = await Block.decode({ bytes, codec, hasher })
const entry = decodedEntry.value
if (decryptPayloadFn) {
try {
const decryptedPayloadBytes = await decryptPayloadFn(entry.payload)
const { value: decryptedPayload } = await Block.decode({ bytes: decryptedPayloadBytes, codec, hasher })
entry._payload = entry.payload
entry.payload = decryptedPayload
} catch (e) {
throw new Error('Could not decrypt payload')
}
}
cid = cid || decodedEntry.cid
const hash = cid.toString(hashStringEncoding)
return {
...value,
hash,
bytes
...entry,
hash
}
}
@ -163,13 +203,28 @@ const decode = async (bytes) => {
* @memberof module:Log~Entry
* @private
*/
const encode = async (entry) => {
const { cid, bytes } = await Block.encode({ value: entry, codec, hasher })
const encode = async (entry, encryptEntryFn, encryptPayloadFn) => {
const e = Object.assign({}, entry)
if (encryptPayloadFn) {
e.payload = e._payload
}
delete e._payload
delete e.hash
let { cid, bytes } = await Block.encode({ value: e, codec, hasher })
if (encryptEntryFn) {
bytes = await encryptEntryFn(bytes)
const encryptedEntry = await Block.encode({ value: bytes, codec, hasher })
cid = encryptedEntry.cid
bytes = encryptedEntry.bytes
}
const hash = cid.toString(hashStringEncoding)
const clock = Clock(entry.clock.id, entry.clock.time)
return {
...entry,
clock,
hash,
bytes
}

View File

@ -9,17 +9,19 @@ import MemoryStorage from '../storage/memory.js'
const DefaultStorage = MemoryStorage
const Heads = async ({ storage, heads }) => {
const Heads = async ({ storage, heads, decryptPayloadFn, decryptEntryFn }) => {
storage = storage || await DefaultStorage()
const put = async (heads) => {
heads = findHeads(heads)
for (const head of heads) {
await storage.put(head.hash, head.bytes)
// Store the entry's hash and nexts
await storage.put(head.hash, head.next)
}
}
const set = async (heads) => {
// TODO: fix storage write fluctuation
await storage.clear()
await put(heads)
}
@ -31,7 +33,6 @@ const Heads = async ({ storage, heads }) => {
}
const newHeads = findHeads([...currentHeads, head])
await set(newHeads)
return newHeads
}
@ -43,9 +44,8 @@ const Heads = async ({ storage, heads }) => {
const iterator = async function * () {
const it = storage.iterator()
for await (const [, bytes] of it) {
const head = await Entry.decode(bytes)
yield head
for await (const [hash, next] of it) {
yield { hash, next }
}
}

View File

@ -10,18 +10,14 @@ import LRU from 'lru'
import PQueue from 'p-queue'
import Entry from './entry.js'
import Clock, { tickClock } from './clock.js'
import Heads from './heads.js'
import ConflictResolution from './conflict-resolution.js'
import MemoryStorage from '../storage/memory.js'
import OplogIndex from './oplog-index.js'
const { LastWriteWins, NoZeroes } = ConflictResolution
const randomId = () => new Date().getTime().toString()
const maxClockTimeReducer = (res, acc) => Math.max(res, acc.clock.time)
// Default storage for storing the Log and its entries. Default: Memory. Options: Memory, LRU, IPFS.
const DefaultStorage = MemoryStorage
// Default AccessController for the Log.
// Default policy is that anyone can write to the Log.
// Signature of an entry will always be verified regardless of AccessController policy.
@ -68,24 +64,23 @@ const Log = async (identity, { logId, logHeads, access, entryStorage, headsStora
if (logHeads != null && !Array.isArray(logHeads)) {
throw new Error('\'logHeads\' argument must be an array')
}
// Set Log's id
const id = logId || randomId()
// Encryption of entries and payloads
encryption = encryption || {}
const encryptPayloadFn = encryption.data?.encrypt
// Access Controller
access = access || await DefaultAccessController()
// Oplog entry storage
const _entries = entryStorage || await DefaultStorage()
// Entry index for keeping track which entries are already in the log
const _index = indexStorage || await DefaultStorage()
// Heads storage
headsStorage = headsStorage || await DefaultStorage()
// Add heads to the state storage, ie. init the log state
const _heads = await Heads({ storage: headsStorage, heads: logHeads })
// Index and storage of entries for this Log
const index = await OplogIndex({ logHeads, entryStorage, indexStorage, headsStorage, encryption })
// Conflict-resolution sorting function
sortFn = NoZeroes(sortFn || LastWriteWins)
encryption = encryption || {}
const { encryptPayloadFn, decryptPayloadFn } = encryption
// Internal queues for processing appends and joins in their call-order
const appendQueue = new PQueue({ concurrency: 1 })
const joinQueue = new PQueue({ concurrency: 1 })
@ -110,8 +105,8 @@ const Log = async (identity, { logId, logHeads, access, entryStorage, headsStora
* @instance
*/
const heads = async () => {
const res = await _heads.all()
return res.sort(sortFn).reverse()
const heads_ = await index.heads()
return heads_.sort(sortFn).reverse()
}
/**
@ -138,21 +133,14 @@ const Log = async (identity, { logId, logHeads, access, entryStorage, headsStora
* @instance
*/
const get = async (hash) => {
const bytes = await _entries.get(hash)
if (bytes) {
const entry = await Entry.decode(bytes)
if (decryptPayloadFn) {
entry.payload = JSON.parse(await decryptPayloadFn(entry.payload))
}
return entry
if (!hash) {
throw new Error('hash is required')
}
return index.get(hash)
}
const has = async (hash) => {
const entry = await _index.get(hash)
return entry != null
return index.has(hash)
}
/**
@ -171,6 +159,7 @@ const Log = async (identity, { logId, logHeads, access, entryStorage, headsStora
// 2. Authorize entry
// 3. Store entry
// 4. return Entry
// Get current heads of the log
const heads_ = await heads()
// Create the next pointers from heads
@ -179,33 +168,28 @@ 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 (encryptPayloadFn) {
data = await encryptPayloadFn(JSON.stringify(data))
}
// Create the entry
const entry = await Entry.create(
identity,
id,
data,
encryptPayloadFn,
tickClock(await clock()),
nexts,
refs
)
// Authorize the entry
const canAppend = await access.canAppend(entry)
if (!canAppend) {
throw new Error(`Could not append entry:\nKey "${identity.hash}" is not allowed to write to the log`)
}
// The appended entry is now the latest head
await _heads.set([entry])
// Add entry to the entry storage
await _entries.put(entry.hash, entry.bytes)
// Add entry to the entry index
await _index.put(entry.hash, true)
// Add the entry to the index (=store and index it)
const hash = await index.setHead(entry)
// Return the appended entry
return entry
return { ...entry, hash }
}
return appendQueue.add(task)
@ -232,9 +216,7 @@ const Log = async (identity, { logId, logHeads, access, entryStorage, headsStora
if (!isLog(log)) {
throw new Error('Given argument is not an instance of Log')
}
if (_entries.merge) {
await _entries.merge(log.storage)
}
await index.storage.merge(log.storage)
const heads = await log.heads()
for (const entry of heads) {
await joinEntry(entry)
@ -317,17 +299,11 @@ const Log = async (identity, { logId, logHeads, access, entryStorage, headsStora
await traverseAndVerify()
/* 4. Add missing entries to the index (=to the log) */
for (const hash of hashesToAdd.values()) {
await _index.put(hash, true)
}
await index.addVerified(hashesToAdd.values())
/* 5. Remove heads which new entries are connect to */
for (const hash of connectedHeads.values()) {
await _heads.remove(hash)
}
await index.removeHeads(connectedHeads.values())
/* 6. Add the new entry to heads (=union with current heads) */
await _heads.add(entry)
await index.addHead(entry)
return true
}
@ -512,9 +488,7 @@ const Log = async (identity, { logId, logHeads, access, entryStorage, headsStora
* @instance
*/
const clear = async () => {
await _index.clear()
await _heads.clear()
await _entries.clear()
await index.clear()
}
/**
@ -523,9 +497,7 @@ const Log = async (identity, { logId, logHeads, access, entryStorage, headsStora
* @instance
*/
const close = async () => {
await _index.close()
await _heads.close()
await _entries.close()
await index.close()
}
/**
@ -581,7 +553,8 @@ const Log = async (identity, { logId, logHeads, access, entryStorage, headsStora
close,
access,
identity,
storage: _entries
storage: index.storage,
encryption
}
}

108
src/oplog/oplog-index.js Normal file
View File

@ -0,0 +1,108 @@
import Entry from './entry.js'
import Heads from './heads.js'
import MemoryStorage from '../storage/memory.js'
// Default storage for storing the Log and its entries. Default: Memory. Options: Memory, LRU, IPFS.
const DefaultStorage = MemoryStorage
const OplogIndex = async ({ logHeads, entryStorage, headsStorage, indexStorage, encryption }) => {
// Setup encryption and decryption functions
const encryptEntryFn = encryption?.replication?.encrypt
const decryptEntryFn = encryption?.replication?.decrypt
const encryptPayloadFn = encryption?.data?.encrypt
const decryptPayloadFn = encryption?.data?.decrypt
// Oplog entry storage
const _entries = entryStorage || await DefaultStorage()
// Entry index for keeping track which entries are already in the log
const _index = indexStorage || await DefaultStorage()
// Heads storage
headsStorage = headsStorage || await DefaultStorage()
// Add heads to the state storage, ie. init the log state
const _heads = await Heads({ storage: headsStorage, heads: logHeads, decryptPayloadFn, decryptEntryFn })
const get = async (hash) => {
const bytes = await _entries.get(hash)
if (bytes) {
const entry = await Entry.decode(bytes, decryptEntryFn, decryptPayloadFn)
return entry
}
}
const getBytes = async (hash) => {
return _entries.get(hash)
}
const has = async (hash) => {
const entry = await _index.get(hash)
return entry != null
}
const heads = async () => {
const heads_ = []
for (const { hash } of await _heads.all()) {
const head = await get(hash)
heads_.push(head)
}
return heads_
}
const setHead = async (entry) => {
const { hash, bytes } = await Entry.encode(entry, encryptEntryFn, encryptPayloadFn)
// The appended entry is now the latest head
await _heads.set([{ hash, ...entry }])
// Add entry to the entry storage
await _entries.put(hash, bytes)
// Add entry to the entry index
await _index.put(hash, true)
return hash
}
const addHead = async (entry) => {
/* 6. Add the new entry to heads (=union with current heads) */
await _heads.add(entry)
return entry.hash
}
const removeHeads = async (hashes) => {
/* 5. Remove heads which new entries are connect to */
for (const hash of hashes) {
await _heads.remove(hash)
}
}
const addVerified = async (hashes) => {
/* 4. Add missing entries to the index (=to the log) */
for (const hash of hashes) {
await _index.put(hash, true)
}
}
const clear = async () => {
await _index.clear()
await _heads.clear()
await _entries.clear()
}
const close = async () => {
await _index.close()
await _heads.close()
await _entries.close()
}
return {
get,
getBytes,
has,
heads,
setHead,
addHead,
removeHeads,
addVerified,
storage: _entries,
clear,
close
}
}
export default OplogIndex

View File

@ -3,6 +3,7 @@ import PQueue from 'p-queue'
import { EventEmitter } from 'events'
import { TimeoutController } from 'timeout-abort-controller'
import pathJoin from './utils/path-join.js'
import { Entry } from './oplog/index.js'
const DefaultTimeout = 30000 // 30 seconds
@ -146,7 +147,8 @@ const Sync = async ({ ipfs, log, events, onSynced, start, timeout }) => {
const sendHeads = (source) => {
return (async function * () {
const heads = await log.heads()
for await (const { bytes } of heads) {
for await (const { hash } of heads) {
const bytes = await log.storage.get(hash)
yield bytes
}
})()
@ -156,7 +158,8 @@ const Sync = async ({ ipfs, log, events, onSynced, start, timeout }) => {
for await (const value of source) {
const headBytes = value.subarray()
if (headBytes && onSynced) {
await onSynced(headBytes)
const entry = await Entry.decode(headBytes, log.encryption.replication?.decrypt, log.encryption.data?.decrypt)
await onSynced(entry)
}
}
if (started) {
@ -220,7 +223,8 @@ const Sync = async ({ ipfs, log, events, onSynced, start, timeout }) => {
const task = async () => {
try {
if (data && onSynced) {
await onSynced(data)
const entry = await Entry.decode(data, log.encryption.replication?.decrypt, log.encryption.data?.decrypt)
await onSynced(entry)
}
} catch (e) {
events.emit('error', e)
@ -240,8 +244,9 @@ const Sync = async ({ ipfs, log, events, onSynced, start, timeout }) => {
* @instance
*/
const add = async (entry) => {
if (started) {
await pubsub.publish(address, entry.bytes)
if (started && entry && entry.hash) {
const bytes = await log.storage.get(entry.hash)
await pubsub.publish(address, bytes)
}
}

View File

@ -1,9 +1,9 @@
import { strictEqual, deepStrictEqual } from 'assert'
import { strictEqual, deepStrictEqual, notEqual } from 'assert'
import { rimraf } from 'rimraf'
import { existsSync } from 'fs'
import { copy } from 'fs-extra'
import Path from 'path'
import { Database, Entry, KeyStore, Identities } from '../src/index.js'
import { Database, KeyStore, Identities } from '../src/index.js'
import LevelStorage from '../src/storage/level.js'
import MemoryStorage from '../src/storage/memory.js'
import testKeysPath from './fixtures/test-keys-path.js'
@ -68,8 +68,12 @@ describe('Database', function () {
describe('Options', () => {
it('uses default directory for headsStorage', async () => {
db = await Database({ ipfs, identity: testIdentity, address: databaseId, accessController })
const op = { op: 'PUT', key: 1, value: 'record 1 on db 1' }
const hash = await db.addOperation(op)
const op1 = { op: 'PUT', key: 1, value: 'record 1 on db 1 version 1' }
const op2 = { op: 'PUT', key: 1, value: 'record 1 on db 1 version 2' }
await db.addOperation(op1)
const hash = await db.addOperation(op2)
const headsPath = Path.join('./orbitdb/', `${databaseId}/`, '/log/_heads/')
@ -79,7 +83,9 @@ describe('Database', function () {
const headsStorage = await LevelStorage({ path: headsPath })
deepStrictEqual((await Entry.decode(await headsStorage.get(hash))).payload, op)
const bytes = Uint8Array.from(await headsStorage.get(hash))
notEqual(bytes.length, 0)
await headsStorage.close()
@ -88,8 +94,11 @@ describe('Database', function () {
it('uses given directory for headsStorage', async () => {
db = await Database({ ipfs, identity: testIdentity, address: databaseId, accessController, directory: './custom-directory' })
const op = { op: 'PUT', key: 1, value: 'record 1 on db 1' }
const hash = await db.addOperation(op)
const op1 = { op: 'PUT', key: 1, value: 'record 1 on db 1 version 1' }
const op2 = { op: 'PUT', key: 1, value: 'record 1 on db 1 version 2' }
await db.addOperation(op1)
const hash = await db.addOperation(op2)
const headsPath = Path.join('./custom-directory/', `${databaseId}/`, '/log/_heads/')
@ -99,7 +108,9 @@ describe('Database', function () {
const headsStorage = await LevelStorage({ path: headsPath })
deepStrictEqual((await Entry.decode(await headsStorage.get(hash))).payload, op)
const bytes = Uint8Array.from(await headsStorage.get(hash))
notEqual(bytes.length, 0)
await headsStorage.close()
@ -110,23 +121,41 @@ describe('Database', function () {
it('uses given MemoryStorage for headsStorage', async () => {
const headsStorage = await MemoryStorage()
db = await Database({ ipfs, identity: testIdentity, address: databaseId, accessController, directory: './orbitdb', headsStorage })
const op = { op: 'PUT', key: 1, value: 'record 1 on db 1' }
const hash = await db.addOperation(op)
const op1 = { op: 'PUT', key: 1, value: 'record 1 on db 1 version 1' }
const op2 = { op: 'PUT', key: 1, value: 'record 1 on db 1 version 2' }
deepStrictEqual((await Entry.decode(await headsStorage.get(hash))).payload, op)
await db.addOperation(op1)
const hash = await db.addOperation(op2)
const bytes = Uint8Array.from(await headsStorage.get(hash))
notEqual(bytes.length, 0)
await db.close()
await headsStorage.close()
await rimraf('./orbitdb')
})
it('uses given MemoryStorage for entryStorage', async () => {
const entryStorage = await MemoryStorage()
db = await Database({ ipfs, identity: testIdentity, address: databaseId, accessController, directory: './orbitdb', entryStorage })
const op = { op: 'PUT', key: 1, value: 'record 1 on db 1' }
const hash = await db.addOperation(op)
const headsStorage = await MemoryStorage()
db = await Database({ ipfs, identity: testIdentity, address: databaseId, accessController, directory: './orbitdb', headsStorage, entryStorage })
const op1 = { op: 'PUT', key: 1, value: 'record 1 on db 1 version 1' }
const op2 = { op: 'PUT', key: 1, value: 'record 1 on db 1 version 2' }
deepStrictEqual((await Entry.decode(await entryStorage.get(hash))).payload, op)
await db.addOperation(op1)
const hash = await db.addOperation(op2)
const e = await entryStorage.get(hash)
const bytes = Uint8Array.from(e)
notEqual(bytes.length, 0)
await db.close()
await entryStorage.close()
await headsStorage.close()
await rimraf('./orbitdb')
})
})

View File

@ -33,7 +33,8 @@ describe('Entry', function () {
it('creates a an empty entry', async () => {
const expectedHash = 'zdpuAsKzwUEa8cz9pkJxxFMxLuP3cutA9PDGoLZytrg4RSVEa'
const entry = await create(testIdentity, 'A', 'hello')
strictEqual(entry.hash, expectedHash)
const { hash } = await Entry.encode(entry)
strictEqual(hash, expectedHash)
strictEqual(entry.id, 'A')
strictEqual(entry.clock.id, testIdentity.publicKey)
strictEqual(entry.clock.time, 0)
@ -47,7 +48,8 @@ describe('Entry', function () {
const expectedHash = 'zdpuAmthfqpHRQjdSpKN5etr1GrreJb7QcU1Hshm6pERnzsxi'
const payload = 'hello world'
const entry = await create(testIdentity, 'A', payload)
strictEqual(entry.hash, expectedHash)
const { hash } = await Entry.encode(entry)
strictEqual(hash, expectedHash)
strictEqual(entry.payload, payload)
strictEqual(entry.id, 'A')
strictEqual(entry.clock.id, testIdentity.publicKey)
@ -81,7 +83,7 @@ describe('Entry', function () {
const payload2 = 'hello again'
const entry1 = await create(testIdentity, 'A', payload1)
entry1.clock = tickClock(entry1.clock)
const entry2 = await create(testIdentity, 'A', payload2, entry1.clock, [entry1])
const entry2 = await create(testIdentity, 'A', payload2, null, entry1.clock, [entry1])
strictEqual(entry2.payload, payload2)
strictEqual(entry2.next.length, 1)
// strictEqual(entry2.hash, expectedHash)
@ -91,7 +93,8 @@ describe('Entry', function () {
it('`next` parameter can be an array of strings', async () => {
const entry1 = await create(testIdentity, 'A', 'hello1')
const entry2 = await create(testIdentity, 'A', 'hello2', null, [entry1.hash])
const { hash } = await Entry.encode(entry1)
const entry2 = await create(testIdentity, 'A', 'hello2', null, null, [hash])
strictEqual(typeof entry2.next[0] === 'string', true)
})
@ -138,7 +141,7 @@ describe('Entry', function () {
it('throws an error if next is not an array', async () => {
let err
try {
await create(testIdentity, 'A', 'hello', null, {})
await create(testIdentity, 'A', 'hello', null, null, {})
} catch (e) {
err = e
}

View File

@ -3,7 +3,6 @@ import { rimraf } from 'rimraf'
import { copy } from 'fs-extra'
import { Log, Entry, Identities, KeyStore, MemoryStorage } from '../../src/index.js'
import testKeysPath from '../fixtures/test-keys-path.js'
import { encrypt, decrypt } from '../utils/encrypt.js'
const { create } = Entry
@ -61,15 +60,21 @@ describe('Log', function () {
})
it('sets one head if multiple are given as params', async () => {
const one = await create(testIdentity, 'A', 'entryA', null, [])
const two = await create(testIdentity, 'A', 'entryB', null, [one.hash])
const three = await create(testIdentity, 'A', 'entryC', null, [two.hash])
const four = await create(testIdentity, 'A', 'entryD', null, [two.hash])
const one = await create(testIdentity, 'A', 'entryA', null, null, [])
const { hash: hash1, bytes: bytes1 } = await Entry.encode(one)
const two = await create(testIdentity, 'A', 'entryB', null, null, [hash1])
const { hash: hash2, bytes: bytes2 } = await Entry.encode(two)
const three = await create(testIdentity, 'A', 'entryC', null, null, [hash2])
const { hash: hash3, bytes: bytes3 } = await Entry.encode(three)
const four = await create(testIdentity, 'A', 'entryD', null, null, [hash3])
const { hash: hash4, bytes: bytes4 } = await Entry.encode(four)
const entryStorage = await MemoryStorage()
await entryStorage.put(one.hash, one.bytes)
await entryStorage.put(two.hash, two.bytes)
await entryStorage.put(three.hash, three.bytes)
await entryStorage.put(four.hash, four.bytes)
await entryStorage.put(hash1, bytes1)
await entryStorage.put(hash2, bytes2)
await entryStorage.put(hash3, bytes3)
await entryStorage.put(hash4, bytes4)
three.hash = hash3
two.hash = hash2
const log = await Log(testIdentity, { logId: 'A', logHeads: [three, three, two, two], entryStorage })
const values = await log.values()
const heads = await log.heads()
@ -79,15 +84,22 @@ describe('Log', function () {
})
it('sets two heads if two given as params', async () => {
const one = await create(testIdentity, 'A', 'entryA', null, [])
const two = await create(testIdentity, 'A', 'entryB', null, [one.hash])
const three = await create(testIdentity, 'A', 'entryC', null, [two.hash])
const four = await create(testIdentity, 'A', 'entryD', null, [two.hash])
const one = await create(testIdentity, 'A', 'entryA', null, null, [])
const { hash: hash1, bytes: bytes1 } = await Entry.encode(one)
const two = await create(testIdentity, 'A', 'entryB', null, null, [hash1])
const { hash: hash2, bytes: bytes2 } = await Entry.encode(two)
const three = await create(testIdentity, 'A', 'entryC', null, null, [hash2])
const { hash: hash3, bytes: bytes3 } = await Entry.encode(three)
const four = await create(testIdentity, 'A', 'entryD', null, null, [hash2])
const { hash: hash4, bytes: bytes4 } = await Entry.encode(four)
const entryStorage = await MemoryStorage()
await entryStorage.put(one.hash, one.bytes)
await entryStorage.put(two.hash, two.bytes)
await entryStorage.put(three.hash, three.bytes)
await entryStorage.put(four.hash, four.bytes)
await entryStorage.put(hash1, bytes1)
await entryStorage.put(hash2, bytes2)
await entryStorage.put(hash3, bytes3)
await entryStorage.put(hash4, bytes4)
three.hash = hash3
four.hash = hash4
two.hash = hash2
const log = await Log(testIdentity, { logId: 'A', logHeads: [three, four, two], entryStorage })
const values = await log.values()
const heads = await log.heads()
@ -143,34 +155,5 @@ describe('Log', function () {
strictEqual(values[1].payload, 'hello2')
strictEqual(values[2].payload, 'hello3')
})
it('encrypts a log entry when the payload is a string', async () => {
const keys = await keystore.createKey('hello1')
const privateKey = await keystore.getKey('hello1')
const publicKey = await keystore.getPublic(keys)
const encryptPayloadFn = encrypt({ publicKey })
const decryptPayloadFn = decrypt({ privateKey })
const log = await Log(testIdentity, { encryption: { encryptPayloadFn, decryptPayloadFn } })
const entry = await log.append('hello1')
const value = await log.get(entry.hash)
strictEqual(value.payload, 'hello1')
})
it('encrypts a log entry when the payload is an object', async () => {
const keys = await keystore.createKey('hello1')
const privateKey = await keystore.getKey('hello1')
const publicKey = await keystore.getPublic(keys)
const encryptPayloadFn = encrypt({ publicKey })
const decryptPayloadFn = decrypt({ privateKey })
const log = await Log(testIdentity, { encryption: { encryptPayloadFn, decryptPayloadFn } })
const entry = await log.append({ test: 'hello1' })
const value = await log.get(entry.hash)
deepStrictEqual(value.payload, { test: 'hello1' })
})
})
})

View File

@ -69,7 +69,7 @@ describe('Log - Replication', function () {
try {
if (!messageIsFromMe(message)) {
const entry = await Entry.decode(message.detail.data)
await storage1.put(entry.hash, entry.bytes)
await storage1.put(entry.hash, message.detail.data)
await log1.joinEntry(entry)
}
} catch (e) {
@ -83,7 +83,7 @@ describe('Log - Replication', function () {
try {
if (!messageIsFromMe(message)) {
const entry = await Entry.decode(message.detail.data)
await storage2.put(entry.hash, entry.bytes)
await storage2.put(entry.hash, message.detail.data)
await log2.joinEntry(entry)
}
} catch (e) {
@ -114,8 +114,10 @@ describe('Log - Replication', function () {
for (let i = 1; i <= amount; i++) {
const entry1 = await input1.append('A' + i)
const entry2 = await input2.append('B' + i)
await ipfs1.libp2p.services.pubsub.publish(logId, entry1.bytes)
await ipfs2.libp2p.services.pubsub.publish(logId, entry2.bytes)
const bytes1 = await input1.storage.get(entry1.hash)
const bytes2 = await input1.storage.get(entry2.hash)
await ipfs1.libp2p.services.pubsub.publish(logId, bytes1)
await ipfs2.libp2p.services.pubsub.publish(logId, bytes2)
}
console.log('Messages sent')

View File

@ -1,29 +1,42 @@
import { strictEqual } from 'assert'
import { strictEqual, notEqual } from 'assert'
import { rimraf } from 'rimraf'
import path from 'path'
import OrbitDB from '../src/orbitdb.js'
// import waitFor from './utils/wait-for.js'
import { createOrbitDB, PasswordEncryption } from '../src/index.js'
// import { encrypt, decrypt, generatePassword } from './utils/encrypt.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 waitFor from './utils/wait-for.js'
import createHelia from './utils/create-helia.js'
import { encrypt, decrypt } from './utils/encrypt.js'
import * as Block from 'multiformats/block'
import * as dagCbor from '@ipld/dag-cbor'
import { sha256 } from 'multiformats/hashes/sha2'
const codec = dagCbor
const hasher = sha256
const dbPath = './orbitdb/tests/write-permissions'
describe('Encryption/Decryption', function () {
this.timeout(20000)
describe('Encryption', function () {
this.timeout(5000)
let ipfs1, ipfs2
let orbitdb1, orbitdb2
let db1 /*, db2 */
let db1, db2
let replicationEncryption
let dataEncryption
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') })
await rimraf('./orbitdb')
orbitdb1 = await createOrbitDB({ ipfs: ipfs1, id: 'user1', directory: path.join(dbPath, '1') })
orbitdb2 = await createOrbitDB({ ipfs: ipfs2, id: 'user2', directory: path.join(dbPath, '2') })
replicationEncryption = await PasswordEncryption({ password: 'hello' })
dataEncryption = await PasswordEncryption({ password: 'world' })
})
after(async () => {
@ -48,41 +61,315 @@ describe('Encryption/Decryption', function () {
await rimraf('./ipfs2')
})
afterEach(async () => {
await db1.drop()
await db1.close()
describe('Data is encrypted when replicated to peers', async () => {
afterEach(async () => {
if (db1) {
await db1.drop()
await db1.close()
}
if (db2) {
await db2.drop()
await db2.close()
}
})
// await db2.drop()
// await db2.close()
it('encrypts/decrypts data', async () => {
let connected = false
let updated = false
let error = false
const encryption = {
data: dataEncryption
}
db1 = await orbitdb1.open('encryption-test-1', { encryption })
db2 = await orbitdb2.open(db1.address, { encryption })
const onJoin = async (peerId, heads) => {
connected = true
}
db2.events.on('join', onJoin)
await waitFor(() => connected, () => true)
const onUpdate = async (peerId, heads) => {
updated = true
}
db2.events.on('update', onUpdate)
const onError = async (err) => {
// Catch "Could not decrypt entry" errors
console.log(err)
error = true
}
db2.events.on('error', onError)
const hash1 = await db1.add('record 1')
const hash2 = await db1.add('record 2')
strictEqual(await db1.get(hash1), 'record 1')
strictEqual(await db1.get(hash2), 'record 2')
await waitFor(() => updated || error, () => true)
const all = await db2.all()
strictEqual(all.length, 2)
strictEqual(all[0].value, 'record 1')
strictEqual(all[1].value, 'record 2')
})
it('encrypts/decrypts log', async () => {
let connected = false
let updated = false
let error = false
const encryption = {
replication: replicationEncryption
}
db1 = await orbitdb1.open('encryption-test-1', { encryption })
db2 = await orbitdb2.open(db1.address, { encryption })
const onJoin = async (peerId, heads) => {
connected = true
}
db2.events.on('join', onJoin)
await waitFor(() => connected, () => true)
const onUpdate = async (peerId, heads) => {
updated = true
}
db2.events.on('update', onUpdate)
const onError = async (err) => {
// Catch "Could not decrypt entry" errors
console.log(err)
error = true
}
db2.events.on('error', onError)
const hash1 = await db1.add('record 1')
const hash2 = await db1.add('record 2')
strictEqual(await db1.get(hash1), 'record 1')
strictEqual(await db1.get(hash2), 'record 2')
await waitFor(() => updated || error, () => true)
const all = await db2.all()
strictEqual(all.length, 2)
strictEqual(all[0].value, 'record 1')
strictEqual(all[1].value, 'record 2')
})
it('encrypts/decrypts log and data', async () => {
let connected = false
let updated = false
let error = false
const encryption = {
replication: replicationEncryption,
data: dataEncryption
}
db1 = await orbitdb1.open('encryption-test-1', { encryption })
db2 = await orbitdb2.open(db1.address, { encryption })
const onJoin = async (peerId, heads) => {
connected = true
}
db2.events.on('join', onJoin)
await waitFor(() => connected, () => true)
const onUpdate = async (peerId, heads) => {
updated = true
}
db2.events.on('update', onUpdate)
const onError = async (err) => {
// Catch "Could not decrypt entry" errors
console.log(err)
error = true
}
db2.events.on('error', onError)
const hash1 = await db1.add('record 1')
const hash2 = await db1.add('record 2')
strictEqual(await db1.get(hash1), 'record 1')
strictEqual(await db1.get(hash2), 'record 2')
await waitFor(() => updated || error, () => true)
const all = await db2.all()
strictEqual(all.length, 2)
strictEqual(all[0].value, 'record 1')
strictEqual(all[1].value, 'record 2')
})
it('throws an error if log can\'t be decrypted', async () => {
let connected = false
let hasError = false
let error
const replicationEncryptionWithWrongPassword = await PasswordEncryption({ password: 'olleh' })
const encryption = {
replication: replicationEncryption
}
const encryptionWithWrongPassword = {
replication: replicationEncryptionWithWrongPassword
}
db1 = await orbitdb1.open('encryption-test-1', { encryption })
db2 = await orbitdb2.open(db1.address, { encryption: encryptionWithWrongPassword })
const onJoin = async (peerId, heads) => {
connected = true
}
db2.events.on('join', onJoin)
await waitFor(() => connected, () => true)
const onError = async (err) => {
// Catch "Could not decrypt entry" errors
error = err
hasError = true
}
db2.events.on('error', onError)
await db1.add('record 1')
await waitFor(() => hasError, () => true)
strictEqual(error.message, 'Could not decrypt entry')
const all = await db2.all()
strictEqual(all.length, 0)
})
it('throws an error if data can\'t be decrypted', async () => {
let connected = false
let hasError = false
let error
const dataEncryptionWithWrongPassword = await PasswordEncryption({ password: 'olleh' })
const encryption = {
data: dataEncryption
}
const encryptionWithWrongPassword = {
data: dataEncryptionWithWrongPassword
}
db1 = await orbitdb1.open('encryption-test-1', { encryption })
db2 = await orbitdb2.open(db1.address, { encryption: encryptionWithWrongPassword })
const onJoin = async (peerId, heads) => {
connected = true
}
db2.events.on('join', onJoin)
await waitFor(() => connected, () => true)
const onError = async (err) => {
// Catch "Could not decrypt entry" errors
error = err
hasError = true
}
db2.events.on('error', onError)
await db1.add('record 1')
await waitFor(() => hasError, () => true)
strictEqual(error.message, 'Could not decrypt payload')
const all = await db2.all()
strictEqual(all.length, 0)
})
})
it('encrypts/decrypts data', async () => {
const keystore = orbitdb1.keystore
const keys = await keystore.createKey('encryption-test')
describe('Data is encrypted in storage', async () => {
afterEach(async () => {
if (db1) {
await db1.drop()
await db1.close()
}
})
const privateKey = await keystore.getKey('encryption-test')
const publicKey = await keystore.getPublic(keys)
it('payload bytes are encrypted in storage', async () => {
let error
const encryptFn = encrypt({ publicKey })
const decryptFn = decrypt({ privateKey })
db1 = await orbitdb1.open('encryption-test-1', { encrypt: { data: { encryptFn, decryptFn } } })
const encryption = {
data: dataEncryption
}
const hash = await db1.add('record 1')
strictEqual(await db1.get(hash), 'record 1')
})
db1 = await orbitdb1.open('encryption-test-1', { encryption })
it('encrypts/decrypts op', async () => {
const keystore = orbitdb1.keystore
const keys = await keystore.createKey('encryption-test')
const onError = async (err) => {
// Catch "Could not decrypt entry" errors
console.log(err)
error = true
}
db1.events.on('error', onError)
const privateKey = await keystore.getKey('encryption-test')
const publicKey = await keystore.getPublic(keys)
const hash1 = await db1.add('record 1')
const encryptFn = encrypt({ publicKey })
const decryptFn = decrypt({ privateKey })
db1 = await orbitdb1.open('encryption-test-1', { encrypt: { op: { encryptFn, decryptFn } } })
const bytes = await db1.log.storage.get(hash1)
const { value } = await Block.decode({ bytes, codec, hasher })
const payload = value.payload
const hash = await db1.add('record 1')
strictEqual(await db1.get(hash), 'record 1')
strictEqual(payload.constructor, Uint8Array)
try {
await Block.decode({ bytes: payload, codec, hasher })
} catch (e) {
error = e
}
strictEqual(error.message.startsWith('CBOR decode error'), true)
})
it('entry bytes are encrypted in storage', async () => {
let error
const encryption = {
replication: replicationEncryption
}
db1 = await orbitdb1.open('encryption-test-1', { encryption })
const onError = async (err) => {
// Catch "Could not decrypt entry" errors
console.log(err)
error = true
}
db1.events.on('error', onError)
const hash1 = await db1.add('record 1')
let decodedBytes
try {
const bytes = await db1.log.storage.get(hash1)
decodedBytes = await Block.decode({ bytes, codec, hasher })
await Block.decode({ bytes: decodedBytes, codec, hasher })
} catch (e) {
error = e
}
notEqual(error, undefined)
strictEqual(error.message.startsWith('CBOR decode error'), true)
strictEqual(decodedBytes.value.constructor, Uint8Array)
})
})
})

View File

@ -139,8 +139,7 @@ describe('Sync protocol', function () {
log1 = await Log(testIdentity1, { logId: 'synclog111', entryStorage: entryStorage1 })
log2 = await Log(testIdentity2, { logId: 'synclog111', entryStorage: entryStorage2 })
const onSynced = async (bytes) => {
const entry = await Entry.decode(bytes)
const onSynced = async (entry) => {
if (await log2.joinEntry(entry)) {
syncedHead = entry
syncedEventFired = true
@ -207,8 +206,7 @@ describe('Sync protocol', function () {
log1 = await Log(testIdentity1, { logId: 'synclog7', entryStorage: entryStorage1 })
log2 = await Log(testIdentity2, { logId: 'synclog7', entryStorage: entryStorage2 })
const onSynced = async (bytes) => {
const entry = await Entry.decode(bytes)
const onSynced = async (entry) => {
if (await log2.joinEntry(entry)) {
syncedHead = entry
}
@ -291,8 +289,8 @@ describe('Sync protocol', function () {
log1 = await Log(testIdentity1, { logId: 'synclog1' })
log2 = await Log(testIdentity2, { logId: 'synclog1' })
const onSynced = async (bytes) => {
syncedHead = await Entry.decode(bytes)
const onSynced = async (entry) => {
syncedHead = entry
syncedEventFired = expectedEntry.hash === syncedHead.hash
}
@ -348,8 +346,8 @@ describe('Sync protocol', function () {
log1 = await Log(testIdentity1, { logId: 'synclog1' })
log2 = await Log(testIdentity2, { logId: 'synclog1' })
const onSynced = async (bytes) => {
syncedHead = await Entry.decode(bytes)
const onSynced = async (entry) => {
syncedHead = entry
if (expectedEntry) {
syncedEventFired = expectedEntry.hash === syncedHead.hash
}
@ -434,9 +432,9 @@ describe('Sync protocol', function () {
log1 = await Log(testIdentity1, { logId: 'synclog1' })
log2 = await Log(testIdentity2, { logId: 'synclog1' })
const onSynced = async (bytes) => {
const onSynced = async (entry) => {
if (expectedEntry && !syncedEventFired) {
syncedHead = await Entry.decode(bytes)
syncedHead = entry
syncedEventFired = expectedEntry.hash === syncedHead.hash
}
}
@ -518,8 +516,8 @@ describe('Sync protocol', function () {
log1 = await Log(testIdentity1, { logId: 'synclog2' })
log2 = await Log(testIdentity2, { logId: 'synclog2' })
const onSynced = async (bytes) => {
syncedHead = await Entry.decode(bytes)
const onSynced = async (entry) => {
syncedHead = entry
if (expectedEntry) {
syncedEventFired = expectedEntry ? expectedEntry.hash === syncedHead.hash : false
}

View File

@ -1,19 +0,0 @@
import EthCrypto from 'eth-crypto'
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
const encrypt = ({ publicKey }) => async (value) => {
const encryptedObj = await EthCrypto.encryptWithPublicKey(publicKey, value)
return EthCrypto.cipher.stringify(encryptedObj)
}
const decrypt = ({ privateKey }) => async (value) => {
const privateKeyStr = uint8ArrayToString(privateKey.marshal(), 'base16')
const encryptedObj = EthCrypto.cipher.parse(value)
return await EthCrypto.decryptWithPrivateKey(privateKeyStr, encryptedObj)
}
export {
encrypt,
decrypt
}