Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@tabcat/encrypted-docstore

Package Overview
Dependencies
Maintainers
1
Versions
10
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@tabcat/encrypted-docstore - npm Package Compare versions

Comparing version 1.0.3 to 3.0.0

test/encryptedDocstore.test.js

21

package.json
{
"name": "@tabcat/encrypted-docstore",
"version": "1.0.3",
"version": "3.0.0",
"description": "mount encrypted docstores with a key",

@@ -10,5 +10,2 @@ "repository": {

"main": "src/index.js",
"browser": {
"./src/node-webcrypto-ossl.js": "./src/webcrypto.js"
},
"keywords": [

@@ -19,9 +16,23 @@ "orbit-db",

],
"scripts": {
"test": "mocha"
},
"standard": {
"env": "mocha",
"ignore": [
"test/**"
]
},
"author": "anderbs@tuta.io",
"license": "MIT",
"dependencies": {
"@tabcat/peer-account-crypto": "0.0.2",
"bs58": "^4.0.1",
"node-webcrypto-ossl": "^1.0.48",
"safe-buffer": "^5.2.0"
},
"devDependencies": {
"@tabcat/ipfs-bundle-t": "0.0.5",
"mocha": "^6.2.1",
"orbit-db": "^0.22.0"
}
}

@@ -6,4 +6,8 @@ # encrypted-docstore

NOTE: *should* work in node but haven't tested at all
NOTE: version 3.0.0 changes how EncryptedDocstore determines the orbitdb address, this is a breaking change. Some changes have been made to the api as well, mostly naming.
TODO: </br>
extend the docstore instead of wrap it. </br>
make every entry iv deterministic? based off anything unique besides orbit id and clock with the goal of having duplicate entries from different nodes collapse.
## Usage

@@ -21,19 +25,10 @@ install with npm:

// create the encryption key
const secret = new TextEncoder().encode('something at least kind of random here')
const salt = await window.crypto.getRandomValues(new Uint8Array(16)) // 128bit salt
const key = EncryptedDocstore.deriveKey(secret, salt)
const aesKey = EncryptedDocstore.generateKey()
// create the docstore with orbit.create:
const dbConfig = { name:'asdf', type:'docstore' }
const encDbName = await EncryptedDocstore.determineEncDbName(orbit, dbConfig, key)
const docstore = await open.create(encDbname, dbConfig.type, dbConfig.options)
const encDocstore = await EncryptedDocstore.mount(docstore, key)
// create the docstore with orbitdb:
const dbConfig = { name:'asdf', type:'docstore', options: {} }
const encAddr = await EncryptedDocstore.determineAddress(orbitdb, dbConfig, aesKey)
const docstore = await orbitdb.docs(encAddr, dbConfig.options)
// OR
// create the docstore with orbit.docs:
const encDbAddress = await EncryptedDocstore.determineEncDbAddress(orbit, dbConfig, key)
const docstore = await orbit.docs(encDbAddress)
const encDocstore = await EncryptedDocstore.mount(docstore, key)
const encDocstore = await EncryptedDocstore.mount(docstore, aesKey)
// get,put, del, query all exposed on encDocstore and returned results should be identical to docstore methods

@@ -47,63 +42,53 @@

### Static Methods:
#### EncDoc.mount(docstore, key)
#### EncDoc.mount(docstore, aesKey)
>mount an encrypted docstore
*docstore:* orbit docstore made with name from EncDoc.determineEncDbName or address from EncDoc.determineEncDbAddress<br/>
*key:* instance of key from src/key.js, made with EncDoc.
*aesKey:* instance of AesKey from generateKey, deriveKey, or importKey static methods.
returns a promise that resolves to an instance of EncDoc
#### EncDoc.determineEncDbName(orbit, dbConfig, key)
>determine the EncDoc name for a docstore config and key
#### EncDoc.determineAddress(orbitdb, dbConfig, aesKey)
>determine the docstore address for the encryptedDocstore, this is adding a way to check the aesKey against the db name
*orbit:* an instance of OrbitDB<br/>
*orbitdb:* an instance of OrbitDB<br/>
*dbConfig:* an object containing name, type and options for an orbit store settings<br/>
*key:* instance of Key from src/key.js, made with EncDoc.deriveKey or EncDoc.importKey<br/>
*aesKey:* instance of AesKey from generateKey, deriveKey, or importKey static methods.<br/>
returns a promise that resolves to a string made of:<br/>
`<encrypted original config address root>/<original config address root>` both fields are base58 encoded
#### EncDoc.determineEncDbAddress(orbit, dbConfig, key)
>determine the EncDoc address for a docstore config and key
returns a promise that resolves to an instance of orbit address
#### EncDoc.keyCheck(encAddr, aesKey)
>check if an orbitdb address and aesKey are a match
*orbit:* an instance of OrbitDB<br/>
*dbConfig:* an object containing name, type and options for an orbit store settings<br/>
*key:* instance of Key from src/key.js, made with EncDoc.deriveKey or EncDoc.importKey<br/>
*encAddr:* instance of orbit address from EncDoc.determineAddress<br/>
*aesKey:* instance of AesKey from generateKey, deriveKey, or importKey static methods.<br/>
returns a promise that resolves to an instance of orbit address
#### EncDoc.keyCheck(address, key)
>check if a key is used for this db address
returns a promise that resolves to a boolean
#### EncDoc.generateKey([length])
>generates a new aesKey
*address:* instance of orbit address<br/>
*key:* instance of Key from src/key.js, made with EncDoc.deriveKey or EncDoc.importKey<br/>
*length:* number, aesKey length, defaults to 128. can be 128, 192, or 256<br/>
returns promise that resolves to a boolean
#### EncDoc.deriveKey(bytes, salt, [length, [purpose]])
>derive instance of Key from bytes and salt
returns an instance of AesKey
#### EncDoc.deriveKey(bytes, salt[, length])
>derive an instance of AesKey from bytes and salt, uses PBKDF2 with 10k iterations
*bytes:* bytes array made from randomness or a strong password<br/>
*salt:* bytes array to be used as salt for deriving the key, recommend using 128bit random value<br/>
*length:* number representing cipherblock size, defaults to 128<br/>
*purpose:* string that is used in generating the key somehow<br/>
*bytes:* Uint8Array made from randomness or a strong password<br/>
*salt:* Uint8Array to be used as salt for deriving the key, optimally a 128bit random value<br/>
*length:* number, aesKey length, defaults to 128. can be 128, 192, or 256<br/>
returns an instance of Key
returns an instance of AesKey
#### EncDoc.importKey(rawKey)
>import a key from raw bytes from EncDoc.exportKey
>import an exported aesKey
*rawKey:* bytes array from EncDoc.exportKey
*rawKey:* Uint8Array from EncDoc.exportKey
returns an instance of Key
#### EncDoc.exportKey(key)
>export a key
returns an instance of AesKey
#### EncDoc.exportKey(aesKey)
>export an aesKey
*key:* instance of Key
*aesKey:* instance of AesKey
returns a bytes array that can be used as rawKey in EncDoc.importKey
returns a Uint8Array rawKey
### Instance Propterties:
#### encDoc.encrypted
> the orbit docstore being used as the encrypted docstore
#### encDoc.key
> an instance of the Key class from src/key.js
### Instance Methods:
- get, put, del, query all work by encapsulating the field it is indexed by (default is \_id) and should behave the same
- get, put, del, query all work by encapsulating the whole doc and pass docstore tests for the orbitdb repo: https://github.com/orbitdb/orbit-db/blob/master/test/docstore.test.js
#### encDoc.get(key)

@@ -113,3 +98,3 @@ see: https://github.com/orbitdb/orbit-db/blob/master/API.md#getkey-1

differences:
- is async function
- is an async function
#### encDoc.put(doc)

@@ -127,9 +112,7 @@ >see: https://github.com/orbitdb/orbit-db/blob/master/API.md#putdoc

differences:
- is async function
- is an async function
- when calling with option fullOp:
+ the payload.value is the decrypted/decapsulated doc.
+ the payload.key which would usually match the payload.value[indexBy] field (indexBy default is '\_id')
does not.
+ the payload.value is the decrypted/decapsulated doc.
+ anything in the fullOp entry relating to hashing the real payload.value will not match the payload.value
- when not calling with option fullOp:
+ no visible differences

@@ -5,78 +5,109 @@

const Buffer = require('safe-buffer').Buffer
const Key = require('./key')
const { aes, util } = require('@tabcat/peer-account-crypto')
// deterministic iv
// used as iv when encrypting dbAddr root for encAddr
const dIv = util.str2ab('encrypted-docstore')
function encryptDoc (aesKey, indexBy) {
return async (doc) => {
try {
if (doc === undefined) {
throw new Error('doc must be defined')
}
const bytes = util.str2ab(JSON.stringify(doc))
const enc = await aesKey.encrypt(bytes)
const cipherbytes = Object.values(enc.cipherbytes)
const iv = Object.values(enc.iv)
return { [indexBy]: `entry-${iv.join('.')}`, cipherbytes, iv }
} catch (e) {
console.error(e)
console.error('failed to encrypt doc')
return undefined
}
}
}
function decryptDoc (aesKey) {
return async (encDoc) => {
try {
if (encDoc === undefined) {
throw new Error('encDoc must be defined')
}
const cipherbytes = new Uint8Array(encDoc.cipherbytes)
const iv = new Uint8Array(encDoc.iv)
const decrypted = await aesKey.decrypt(cipherbytes, iv)
const doc = JSON.parse(util.ab2str(decrypted.buffer))
return { internal: doc, external: encDoc }
} catch (e) {
console.error(e)
console.error('failed to decrypt doc')
return undefined
}
}
}
function decryptDocs (aesKey) {
const decrypt = decryptDoc(aesKey)
return async (encDocs) => {
const docs = await Promise.all(encDocs.map(encDoc => decrypt(encDoc)))
return docs.filter(t => t) // prune undefined
}
}
class EncryptedDocstore {
constructor(encryptedDocstore, key) {
if (!encryptedDocstore) throw new Error('encryptedDocstore must be defined')
if (!key) throw new Error('key must be defined')
this._docstore = encryptedDocstore
this._key = key
this.indexBy = this._docstore.options.indexBy
constructor (docstore, aesKey) {
if (!docstore) throw new Error('docstore must be defined')
if (!aesKey) throw new Error('aesKey must be defined')
this._docstore = docstore
this._aesKey = aesKey
this._indexBy = this._docstore.options.indexBy
}
// docstore: an instance of orbitdb docstore with name from determineEncDbName
// (get this by opening a docstore by address or creating one with a config)
// key: aes cryptoKey (get this from this.deriveKey)
static async mount(docstore, key) {
if (key === undefined || docstore === undefined) {
throw new Error('key and encryptedDocstore must be defined')
static async mount (docstore, aesKey) {
if (!docstore || !aesKey) {
throw new Error('docstore and aesKey must be defined')
}
// if keyCheck fails throw
if (!(await this.keyCheck(docstore.address, key))) {
if (!await this.keyCheck(docstore.address, aesKey)) {
throw new Error('keyCheck failed while trying to mount store')
}
return new EncryptedDocstore(docstore, key)
return new EncryptedDocstore(docstore, aesKey)
}
// use to get name of encDocstore for creating the docstore
static async determineEncDbName(orbitdb, dbConfig, key) {
if (orbitdb === undefined || dbConfig === undefined || key === undefined) {
throw new Error('orbitdb, dbConfig and key must be defined')
// use to determine address of the docstore to be used for encryption
static async determineAddress (orbitdb, dbConfig, aesKey) {
if (!orbitdb || !dbConfig || !aesKey) {
throw new Error('orbitdb, dbConfig and aesKey must be defined')
}
if (dbConfig.type !== 'docstore') {
throw new Error('dbConfig type must be docstore')
}
if (!dbConfig.name) throw new Error('')
const { name, type, options } = dbConfig
const root = (await orbitdb.determineAddress(name, type, options)).root
const decodedRoot = bs58.decode(root)
const encRoot = bs58.encode(
Buffer.from(
(await key.encrypt(decodedRoot, decodedRoot)).cipherbytes
)
)
return `${encRoot}/${root}`
}
// use to determine address of encDocstore
static async determineEncDbAddress(orbitdb, dbConfig, key) {
if (orbitdb === undefined || dbConfig === undefined || key === undefined) {
throw new Error('orbitdb, dbConfig and key must be defined')
try {
const encRoot = bs58.encode(Buffer.from(
(await aesKey.encrypt(decodedRoot, dIv)).cipherbytes
))
const encName = `${encRoot}/${name}`
return orbitdb.determineAddress(encName, type, options)
} catch (e) {
console.error(e)
throw new Error('failed to determine address')
}
if (dbConfig.type !== 'docstore') {
throw new Error('dbConfig type must be docstore')
}
const encDbName = await this.determineEncDbName(orbitdb, dbConfig, key)
return await orbitdb.determineAddress(encDbName, dbConfig.type, dbConfig.options)
}
static async keyCheck(address, key) {
if (address === undefined || key === undefined) {
throw new Error('address and key must be defined')
// encAddr is whats returned from this.determineAddress
// it is an instance of orbitdb's address
static async keyCheck (encAddr, aesKey) {
if (!encAddr || !aesKey) {
throw new Error('encAddr and aesKey must be defined')
}
const [ encRoot, root ] = address.path.split('/')
const decodedEncRoot = bs58.decode(encRoot)
// d for deterministic
// probably the weakest part of the encryptedDocstore to precomputed tables.
// weakness is related to there being basically no salt so attacker needs
// a table with an entry of every encrypted original root from encrypting every
// every likely original root with every possible aes-gcm key to know your key?
// seems like a lot but targeting specific orbitdb stores would require only
// finding every encrypted original root by encrypting one orignal root with
// every possible aes-gcm key (if the amateur cryptographer in me is correct)
// hopefully i am correct in thinking this is adequate security for now
// if the bytes deriving the aes key are random enough
const dIv = bs58.decode(root)
const encRoot = encAddr.path.split('/')[0]
if (!encRoot) throw new Error('invalid encrypted docstore address')
try {
const decrypted = await key.decrypt(decodedEncRoot, dIv)
return bs58.encode(Buffer.from(decrypted)) === root
} catch(e) {
console.error(e)
await aesKey.decrypt(bs58.decode(encRoot), dIv)
return true
} catch (e) {
return false

@@ -86,27 +117,21 @@ }

// cryptographic opterations imported from ./key.js
// creates an AES key that can be used for an encryptedDocstore
// require bytes source to have sufficient entropy when implementing
static async genKey(...params) {
return await Key.genKey(...params)
// creates an aesKey that can be used for an encryptedDocstore
static async generateKey (...params) {
return aes.generateKey(...params)
}
static async deriveKey(...params) {
return await Key.deriveKey(...params)
static async deriveKey (...params) {
return aes.deriveKey(...params)
}
static async importKey(...params) {
return await Key.importKey(...params)
static async importKey (...params) {
return aes.importKey(...params)
}
static async exportKey(...params) {
return await Key.exportKey(...params)
}
// helpers
async decryptRecords(encryptedRecords) {
return await Promise.all(
encryptedRecords.map(encDoc => this._key.decryptMsg(encDoc))
)
static async exportKey (...params) {
return aes.exportKey(...params)
}
// docstore operations
async get(indexKey, caseSensitive = false) {
// docstore operations
async get (indexKey, caseSensitive = false) {
if (indexKey === undefined) {

@@ -119,7 +144,7 @@ throw new Error('indexKey is undefined')

const terms = indexKey.split(' ')
const replaceAll = (str, search, replacement) =>
str.toString().split(search).join(replacement)
indexKey = terms.length > 1
? replaceAll(indexKey, '.', ' ').toLowerCase()
: indexKey.toLowerCase()
const replaceAll = (str, search, replacement) =>
str.toString().split(search).join(replacement)
const search = (e) => {

@@ -132,57 +157,69 @@ if (terms.length > 1) {

const filter = caseSensitive
? (i) => i[this.indexBy].indexOf(indexKey) !== -1
: (i) => search(i[this.indexBy])
const records = await this.decryptRecords(this._docstore.query(() => true))
return await Promise.all(records.map(res => res.internal).filter(filter))
? (i) => i[this._indexBy].indexOf(indexKey) !== -1
: (i) => search(i[this._indexBy])
const docs =
await decryptDocs(this._aesKey)(this._docstore.query(() => true))
return Promise.all(docs.map(res => res.internal).filter(filter))
}
async put(doc) {
async put (doc) {
if (typeof doc !== 'object') {
throw new Error('doc must have type of object')
}
if (!doc[this.indexBy]) {
throw new Error(`doc requires an ${this.indexBy} field`)
if (!doc[this._indexBy]) {
throw new Error(`doc requires an ${this._indexBy} field`)
}
// since real _id is encapsulated in cipherbytes field and external _id is
// random, we must delete the old entry by querying for the same id
try { await this.del(doc[this.indexBy]) } catch(e) {}
return await this._docstore.put(await this._key.encryptMsg(doc))
try { await this.del(doc[this._indexBy]) } catch (e) {}
const encDoc = await encryptDoc(this._aesKey, this._indexBy)(doc)
if (!encDoc) throw new Error('failed to encrypt doc')
return this._docstore.put(encDoc)
}
async del(indexKey) {
async del (indexKey) {
if (indexKey === undefined) {
throw new Error('indexKey must be defined')
}
const records = await this.decryptRecords(this._docstore.query(() => true))
const matches = records.filter(res => res.internal[this.indexBy] === indexKey)
// if a deletion fails this will clean it up old records
const docs =
await decryptDocs(this._aesKey)(this._docstore.query(() => true))
const matches = docs.filter(res => res.internal[this._indexBy] === indexKey)
if (matches.length > 1) {
console.error(`there was more than one entry with internal key ${indexKey}`)
console.error(
`there was more than one entry with internal key: ${indexKey}`
)
}
if (matches.length === 0) {
throw new Error(`No entry with key '${indexKey}' in the database`)
throw new Error(`No entry with key: '${indexKey}' in the database`)
}
// if a deletion failed this will clean it up old docs
// only return first deletion to keep same api as docstore
return Promise.all(
matches.map(res => this._docstore.del(res.external[this.indexBy]))
matches.map(res => this._docstore.del(res.external[this._indexBy]))
).then(arr => arr[0])
}
async query(mapper, options = {}) {
if (mapper === undefined) {
throw new Error('mapper was undefined')
async query (mapper, options = {}) {
if (mapper === undefined) throw new Error('mapper was undefined')
if (typeof mapper !== 'function') throw new Error('mapper must be function')
const decrypt = decryptDoc(this._aesKey)
const decryptFullOp = async (entry) => {
const doc = await decrypt(entry.payload.value).then(res => res.internal)
return doc
? {
...entry,
payload: {
...entry.payload,
key: doc[this._indexBy],
value: doc
}
}
: undefined
}
const fullOp = options.fullOp || false
const decryptFullOp = async(entry) => ({
...entry,
payload: {
...entry.payload,
value:await this._key.decryptMsg(entry.payload.value).then(res => res.internal),
},
})
const index = this._docstore._index
const indexGet = fullOp
? async(_id) => decryptFullOp(index._index[_id])
: async(_id) => index._index[_id]
? await this._key.decryptMsg(index._index[_id].payload.value)
const indexGet = options.fullOp || false
? async (_id) => decryptFullOp(index._index[_id])
: async (_id) => index._index[_id]
? decrypt(index._index[_id].payload.value)
.then(res => res.internal)

@@ -192,8 +229,7 @@ : null

return Promise.all(indexKeys.map(key => indexGet(key)))
.then(arr => arr.filter(mapper))
// remove undefined docs before handing to mapper
.then(arr => arr.filter(t => t).filter(mapper))
}
}
module.exports = EncryptedDocstore
'use strict'
module.exports = require('./encryptedDocstore')
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc