crypto-pouch
crypto-pouch copied to clipboard
getAttachment doesn't work
Steps to reproduce:
Create a database Crypto it Replicate in a document with an attachment Call getAttachment on the document
--> CustomPouchError
yes this is true, though it is an issue with transform-pouch which we use under the hood.
I’m looking into this :)
I tried to reproduce the issue as I understood it, but had no luck:
- It creates two in-memory database
- It cryptos
db2
- in
db1
, it creates a doc with an attachment and replicates it todb2
- it reads out the attachment in
db2
once the doc arrived withgetAttachment
var PouchDB = require('pouchdb').defaults({
db: require('memdown')
})
var crypto = require('crypto-pouch')
PouchDB.plugin(crypto)
var db1 = new PouchDB('db1')
var db2 = new PouchDB('db2')
db2.crypto('secretSecureEncryptionKey')
.then(handleDb2Changes)
.then(function () {
var attachment =
'TGVnZW5kYXJ5IGhlYXJ0cywgdGVhciB1cyBhbGwgYXBhcnQKTWFrZS' +
'BvdXIgZW1vdGlvbnMgYmxlZWQsIGNyeWluZyBvdXQgaW4gbmVlZA=='
return db1.putAttachment('id-12345678', 'att.txt', attachment, 'text/plain')
})
.then(function () {
return db1.get('id-12345678')
})
.catch(function (error) {
console.log('\n db1: error ==============================')
console.log(error)
})
.then(function (doc) {
console.log('\ndb1: doc created ==============================')
console.log(doc)
db1.replicate.to('db2')
})
function handleDb2Changes () {
var changes = db2.changes({
live: true,
include_docs: true
}).on('change', function (change) {
console.log('\ndb2: change ==============================')
console.log(change)
changes.cancel()
db2.getAttachment('id-12345678', 'att.txt')
.then(function (attachment) {
console.log('\ndb2: attachment ==============================')
console.log(attachment.toString())
return db1.getAttachment('id-12345678', 'att.txt')
})
.then(function (attachment) {
console.log('\ndb1: attachment ==============================')
console.log(attachment.toString())
})
.catch(console.log)
})
}
The output is as expected:
db2: change ==============================
{ id: 'id-12345678',
changes: [ { rev: '1-a7e9e63a61f1b54394da4d199082dba5' } ],
doc:
{ _id: 'id-12345678',
_rev: '1-a7e9e63a61f1b54394da4d199082dba5',
_attachments: { 'att.txt': [Object] } },
seq: 1 }
db2: attachment ==============================
Legendary hearts, tear us all apart
Make our emotions bleed, crying out in need
db1: attachment ==============================
Legendary hearts, tear us all apart
Make our emotions bleed, crying out in need
Did I do something wrong? @mikeymckay
However, .getAttachement
fails right after .putAttachment
when .crypto(key)
is applied:
var PouchDB = require('pouchdb').defaults({
db: require('memdown')
})
var crypto = require('crypto-pouch')
PouchDB.plugin(crypto)
var db = new PouchDB('db')
db.crypto('secretSecureEncryptionKey')
.then(function () {
var attachment =
'TGVnZW5kYXJ5IGhlYXJ0cywgdGVhciB1cyBhbGwgYXBhcnQKTWFrZS' +
'BvdXIgZW1vdGlvbnMgYmxlZWQsIGNyeWluZyBvdXQgaW4gbmVlZA=='
return db.putAttachment('id-12345678', 'att.txt', attachment, 'text/plain')
})
.then(function () {
return db.getAttachment('id-12345678', 'att.txt')
})
.then(function (attachment) {
console.log('\nattachment ==============================')
console.log(attachment.toString())
})
.catch(console.log)
Logs
{ [not_found: missing]
status: 404,
name: 'not_found',
message: 'missing',
error: true }
It looks like the problem is that crypto-pouch
is wrapping most of the PouchDB APIs with transform-pouch
. But .getAttachment
is not calling the wrapped .get
internally, so res.doc
here is the encrypted string and not an object, so that res.doc._attachments
is undefined and therefore it return callback(createError(MISSING_DOC));
I’m not yet sure what the best way to workaround this could be
This might not be something transform-pouch can even solve, as currently written. I don't know if a wrapped function can intercept that particular get
call.
This might need a change to Pouch core.
FWIW if you get
with binary: true, attachment: true
it's basically the same thing as getAttachment
.
Okay, so the workaround for this is:
Don’t use db.putAttachment
/ db.getAttachment
, instead use db.put
/ db.get
Based on my example code above, the workaround looks like this:
var PouchDB = require('pouchdb').defaults({
db: require('memdown')
})
var crypto = require('crypto-pouch')
PouchDB.plugin(crypto)
var db = new PouchDB('db')
db.crypto('secretSecureEncryptionKey')
.then(function () {
// workaround to use db.put instead of db.putAttachment
// https://github.com/calvinmetcalf/crypto-pouch/issues/13
return db.put({
_id: 'id-12345678',
_attachments: {
'att.txt': {
content_type: 'text/plain',
data: 'TGVnZW5kYXJ5IGhlYXJ0cywgdGVhciB1cyBhbGwgYXBhcnQKTWFrZS' +
'BvdXIgZW1vdGlvbnMgYmxlZWQsIGNyeWluZyBvdXQgaW4gbmVlZA=='
}
}
})
})
.then(function () {
// workaround using db.get instead of db.getAttachment
// https://github.com/calvinmetcalf/crypto-pouch/issues/13
return db.get('id-12345678', {
attachments: true,
binary: true
}).then(function (doc) {
return doc._attachments['att.txt'].data
})
})
.then(function (attachment) {
console.log('\nattachment ==============================')
console.log(attachment)
})
Shall I send a PR to mention that as known issue with a link to the workaround in the README?
The workaround returns base64 while getAttachment
returns a blob.
sorry, got the options wrong above, instead of
attachments: {
binary: true
}
it's
attachments: true,
binary: true
I’ve updated the code above
@nolanlawson I got confused by the indentation of options.binary
. Is that a mistake?

Stumbled upon that, too, but I guess it's intended because binary
changes how attachments are loaded.
@gr2m the updated workaround still returns base64 for me. It returns a buffer if I don't do crypto
.
try this in Chrome
var db = new PouchDB('db')
var id = 'id-' + Math.random().toString(36).substr(2)
db // .crypto('secretSecureEncryptionKey')
.then(function () {
var attachment = new Blob(['Is there life on Mars?'], {type: 'text/plain'});
return db.put({
_id: id,
_attachments: {
'att.txt': {
content_type: 'text/plain',
data: attachment
}
}
})
})
.then(function () {
return db.get(id, {
attachments: true,
binary: true
}).then(function (doc) { return doc._attachments['att.txt'].data})
})
.catch(function (error) {
console.log('\n db1: error ==============================')
console.log(error)
})
.then(function (doc) {
console.log('\ndb1: doc created ==============================')
console.log(doc)
})
With the crypto I get a RangeError: Trying to access beyond buffer
now, checking
this works around the issue for node: https://github.com/calvinmetcalf/crypto-pouch/pull/18.
I’m still getting the RangeError: Trying to access beyond buffer
error in Chrome, but it’s not related to attachments. I get it when running this:
var db = new PouchDB('db')
var id = PouchDB.utils.uuid()
db.crypto('secretSecureEncryptionKey')
.then(function () {
return db.put({
_id: 'id123',
foo: 'bar'
})
})
// Uncaught (in promise) RangeError: Trying to access beyond buffer length
// at checkOffset (<anonymous>:7598:11)
// at Buffer.readUInt32LE (<anonymous>:7621:5)
// at new Chacha20 (<anonymous>:8280:26)
// at new Cipher (<anonymous>:8144:17)
// at Object.createCipher (<anonymous>:8244:10)
// at Object.encrypt [as incoming] (<anonymous>:22600:25)
// at incoming (<anonymous>:21133:21)
// at handlers.bulkDocs (<anonymous>:21183:22)
// at callHandlers (<anonymous>:22183:17)
// at ee.bulkDocs (<anonymous>:21925:12)
2nd attempt: https://github.com/calvinmetcalf/crypto-pouch/pull/25 – now it throws an error when you try to put a doc with _attachments
, and it adds options.ignore
to define properties that you do not want to be encrypted. Let me know what you think
@mikeymckay can you please try again with latest versions of PouchDB / transform-pouch / crypto-pouch? I can’t reproduce the issue any longer
Hey so, it's been five years... but I think we can encrypt attachments.
Regarding the workaround from @gr2m , we could wrap the .putAttachment
and .getAttachment
methods to use the listed workarounds. For example, here's a simple .putAttachment
:
// this === a pouchdb instance
const _putAttachment = this._putAttachment
this.putAttachment = async function (docId, attachId, rev, blob, type, cb) {
// normalize args; stolen from https://github.com/pouchdb/pouchdb/blob/b2db075efb36de08e5cdcdbbaa00fd485f78d3b8/src/adapters/pouch.http.js#L446-L456
if (typeof type === 'function') {
callback = type
type = blob
blob = rev
rev = undefined
}
if (typeof type === 'undefined') {
type = blob
blob = rev
rev = undefined
}
// now get the current doc
const opts = { rev, attachments: true }
const doc = await this.get(docId, opts)
// encrypt the blob and apply it as an inline put
doc._attachments[attachId] = blob
return this.put(doc)
}
This punts encryption duty to .put
, which is fine because transform-pouch helps us wrap that. Then, this block...
if (doc._attachments && !this._ignore.includes('_attachments')) {
throw new Error('Attachments cannot be encrypted. Use {ignore: "_attachments"} option')
}
...can be replaced with something like this:
for (const [attachId, blob] of doc._attachments) {
if (typeof blob !== 'string') {
// only encrypt blobs
const blob64 = encodeBase64(blob)
const encryptedBlob64 = await this._crypt.encrypt(blob64)
doc._attachments[attachId] = encryptedBlob64
}
}
Then you can do the inverse in the outgoing
handler whenever the doc has an ._attachments
property:
for (let [attachId, encryptedBlob64] of doc._attachments) {
const blob64 = await this._crypt.decrypt(encryptedBlob64)
if (options.binary) {
const blob = decodeBase64(blob64)
doc._attachments[attachId] = blob
} else {
doc._attachments[attachId] = blob64 // attachments are normally base64-encoded strings anyway
}
}
Or something like that. That garbles the base64 strings that the database stores but doesn't do any other futzing with content types.
Comments?