@tabcat/encrypted-docstore
Advanced tools
Comparing version 0.0.1 to 0.0.2
{ | ||
"name": "@tabcat/encrypted-docstore", | ||
"version": "0.0.1", | ||
"version": "0.0.2", | ||
"description": "mount encrypted docstores with a key", | ||
@@ -5,0 +5,0 @@ "main": "src/encryptedDocstore.js", |
@@ -6,2 +6,3 @@ # encrypted-docstore | ||
## Usage | ||
@@ -36,3 +37,3 @@ install with npm: | ||
// get,put, del, query all exposed on encDocstore and return results VERY similar orbitDocstore | ||
// get,put, del, query all exposed on encDocstore and returned results should be identical to docstore methods | ||
@@ -44,3 +45,3 @@ ``` | ||
### Static Properties: | ||
### Static Methods: | ||
#### EncDoc.mount(docstore, key) | ||
@@ -100,24 +101,34 @@ >mount an encrypted docstore | ||
### Instance Propterties: | ||
#### encDoc.get(_id) | ||
#### 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 | ||
#### encDoc.get(key) | ||
see: https://github.com/orbitdb/orbit-db/blob/master/API.md#getkey-1 | ||
differences: | ||
- returns doc with EncDoc doc properties ciphertext and iv, do not use these property names for your docs they will be overwritten | ||
no visible differences | ||
#### encDoc.put(doc) | ||
>see: https://github.com/orbitdb/orbit-db/blob/master/API.md#putdoc | ||
differences: | ||
- returns doc with EncDoc doc properties ciphertext and iv, do not use these property names for your docs they will be overwritten | ||
- doc is taken, properties than are not _id, ciphertext or iv are grabbed and encrypted to make the new ciphertext and iv | ||
#### encDoc.del(_id) | ||
no visible differences | ||
#### encDoc.del(key) | ||
>see: https://github.com/orbitdb/orbit-db/blob/master/API.md#delkey-1 | ||
no differences | ||
no visible differences | ||
#### encDoc.query(mapper) | ||
>see: https://github.com/orbitdb/orbit-db/blob/master/API.md#querymapper | ||
no visisble differences: | ||
- mapper is fed unencrypted docs like those returned from the get method | ||
differences: | ||
- 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. | ||
+ 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 | ||
@@ -42,3 +42,3 @@ | ||
Buffer.from( | ||
(await key.encrypt(decodedRoot, decodedRoot)).bytes | ||
(await key.encrypt(decodedRoot, decodedRoot)).cipherbytes | ||
) | ||
@@ -104,11 +104,40 @@ ) | ||
// helpers | ||
async decryptRecords(encryptedRecords) { | ||
return await Promise.all( | ||
encryptedRecords.map(encDoc => this.key.decryptMsg(encDoc)) | ||
) | ||
} | ||
// docstore operations | ||
async get(_id) { | ||
if (_id === undefined) { | ||
throw new Error('_id is undefined') | ||
async get(indexKey, caseSensitive = false) { | ||
if (indexKey === undefined) { | ||
throw new Error('indexKey is undefined') | ||
} | ||
return await Promise.all( | ||
this.encrypted.get(_id).map((doc) => this._id.decryptMsg(doc)) | ||
) | ||
const indexBy = this.encrypted.options.indexBy | ||
// code taken from orbit document store get method: | ||
// https://github.com/orbitdb/orbit-db-docstore/blob/master/src/DocumentStore.js | ||
indexKey = indexKey.toString() | ||
const terms = indexKey.split(' ') | ||
indexKey = terms.length > 1 | ||
? replaceAll(indexKey, '.', ' ').toLowerCase() | ||
: indexKey.toLowerCase() | ||
const replaceAll = (str, search, replacement) => | ||
str.toString().split(search).join(replacement) | ||
const search = (e) => { | ||
if (terms.length > 1) { | ||
return replaceAll(e, '.', ' ').toLowerCase().indexOf(indexKey) !== -1 | ||
} | ||
return e.toLowerCase().indexOf(indexKey) !== -1 | ||
} | ||
const filter = caseSensitive | ||
? (i) => i[indexBy].indexOf(indexKey) !== -1 | ||
: (i) => search(i[indexBy]) | ||
const records = await this.decryptRecords(this.encrypted.query(() => true)) | ||
return await Promise.all(records.map(res => res.internal).filter(filter)) | ||
} | ||
@@ -120,14 +149,40 @@ | ||
} | ||
if (!doc._id) { | ||
throw new Error('doc requires an _id field') | ||
const indexBy = this.encrypted.options.indexBy | ||
if (!doc[this.encrypted.options.indexBy]) { | ||
throw new Error(`doc requires an ${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[indexBy]) } catch(e) {} | ||
return await this.encrypted.put(await this.key.encryptMsg(doc)) | ||
} | ||
async del(key) { | ||
return await this.encrypted.del(key) | ||
async del(indexKey) { | ||
if (indexKey === undefined) { | ||
throw new Error('indexKey must be defined') | ||
} | ||
const indexBy = this.encrypted.options.indexBy | ||
const records = await this.decryptRecords(this.encrypted.query(() => true)) | ||
const matches = records.filter(res => res.internal[indexBy] === indexKey) | ||
// if a deletion fails this will clean it up old records | ||
if (matches.length > 1) { | ||
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`) | ||
} | ||
// only return first deletion to keep same api as docstore | ||
return Promise.all( | ||
matches.map(res => this.encrypted.del(res.external[indexBy])) | ||
).then(arr => arr[0]) | ||
} | ||
async query(mapper) { | ||
async query(mapper, options = {}) { | ||
if (mapper === undefined) { | ||
@@ -137,10 +192,22 @@ throw new Error('mapper was undefined') | ||
// decrypts each doc before mapper recieves them | ||
const encMapper = async (encDoc) => mapper(await this.key.decryptMsg(encDoc)) | ||
// calls the query with the higher order decrypting mapper | ||
const encQuery = this.encrypted.query(encMapper) | ||
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), | ||
}, | ||
}) | ||
return await Promise.all( | ||
encQuery.map((encDoc) => this.key.decryptMsg(encDoc)) | ||
) | ||
const index = this.encrypted._index | ||
const indexGet = fullOp | ||
? async(_id) => decryptFullOp(index._index[_id]) | ||
: async(_id) => index._index[_id] | ||
? await this.key.decryptMsg(index._index[_id].payload.value) | ||
.then(res => res.internal) | ||
: null | ||
const indexKeys = Object.keys(index._index) | ||
return Promise.all(indexKeys.map(key => indexGet(key))) | ||
.then(arr => arr.filter(mapper)) | ||
} | ||
@@ -147,0 +214,0 @@ |
@@ -16,3 +16,3 @@ | ||
const randomBytes = async (bytes) => await crypto.getRandomValues(new Uint8Array(bytes)) | ||
const randomBytes = async (bytesLength) => await crypto.getRandomValues(new Uint8Array(bytesLength)) | ||
@@ -98,7 +98,7 @@ const encDocFields = ['_id', 'ciphertext', 'iv'] | ||
const _bytes = await crypto.subtle.encrypt(algo, this.key, bytes) | ||
const ciphertext = ab2str(_bytes) | ||
const cipherbytes = new Uint8Array( | ||
await crypto.subtle.encrypt(algo, this.key, bytes) | ||
) | ||
return { ciphertext, bytes:_bytes, iv } | ||
return { cipherbytes, iv } | ||
} | ||
@@ -130,7 +130,6 @@ async decrypt(bytes, iv) { | ||
// makes new obj, stringifies and encodes only the encrypted fields | ||
const bytes = str2ab(JSON.stringify(cipherSect(doc))) | ||
const { ciphertext, iv } = await this.encrypt(bytes) | ||
const bytes = str2ab(JSON.stringify(doc)) | ||
const { cipherbytes, iv } = await this.encrypt(bytes) | ||
return { _id:doc._id, ciphertext, iv } | ||
return { _id:`entry-${iv.join('')}`, cipherbytes, iv } | ||
} | ||
@@ -142,12 +141,8 @@ async function decryptDoc(encDoc) { | ||
const bytes = str2ab(encDoc.ciphertext) | ||
const decrypted = await this.decrypt(bytes, encDoc.iv) | ||
// all fields within ciphertext doc field | ||
const decrypted = await this.decrypt(encDoc.cipherbytes, encDoc.iv) | ||
const clearObj = JSON.parse(ab2str(decrypted)) | ||
// clearObj spread first because maybe someone adds iv and ciphertext fields | ||
// inside the ciphertext area for some weird reason and breaks something | ||
return { ...clearObj, ...encDoc } | ||
return { internal:clearObj, external:encDoc } | ||
} | ||
module.exports = Key |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
17063
294
131