crypto-pouch icon indicating copy to clipboard operation
crypto-pouch copied to clipboard

getAttachment doesn't work

Open mikeymckay opened this issue 9 years ago • 17 comments

Steps to reproduce:

Create a database Crypto it Replicate in a document with an attachment Call getAttachment on the document

--> CustomPouchError

mikeymckay avatar Nov 04 '15 08:11 mikeymckay

yes this is true, though it is an issue with transform-pouch which we use under the hood.

calvinmetcalf avatar Nov 05 '15 16:11 calvinmetcalf

I’m looking into this :)

gr2m avatar Feb 12 '16 21:02 gr2m

I tried to reproduce the issue as I understood it, but had no luck:

  1. It creates two in-memory database
  2. It cryptos db2
  3. in db1, it creates a doc with an attachment and replicates it to db2
  4. it reads out the attachment in db2 once the doc arrived with getAttachment
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 }

gr2m avatar Feb 12 '16 21:02 gr2m

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

gr2m avatar Feb 12 '16 22:02 gr2m

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.

nolanlawson avatar Feb 13 '16 07:02 nolanlawson

FWIW if you get with binary: true, attachment: true it's basically the same thing as getAttachment.

nolanlawson avatar Feb 13 '16 07:02 nolanlawson

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?

gr2m avatar Feb 18 '16 22:02 gr2m

The workaround returns base64 while getAttachment returns a blob.

xMartin avatar Feb 19 '16 08:02 xMartin

sorry, got the options wrong above, instead of

    attachments: {
      binary: true
    }

it's

    attachments: true,
    binary: true

I’ve updated the code above

gr2m avatar Feb 19 '16 19:02 gr2m

@nolanlawson I got confused by the indentation of options.binary. Is that a mistake?

screen shot 2016-02-19 at 14 04 35

gr2m avatar Feb 19 '16 19:02 gr2m

Stumbled upon that, too, but I guess it's intended because binary changes how attachments are loaded.

xMartin avatar Feb 19 '16 19:02 xMartin

@gr2m the updated workaround still returns base64 for me. It returns a buffer if I don't do crypto.

xMartin avatar Feb 19 '16 19:02 xMartin

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

gr2m avatar Feb 19 '16 19:02 gr2m

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)

gr2m avatar Feb 19 '16 20:02 gr2m

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

gr2m avatar Apr 05 '16 12:04 gr2m

@mikeymckay can you please try again with latest versions of PouchDB / transform-pouch / crypto-pouch? I can’t reproduce the issue any longer

gr2m avatar May 10 '16 14:05 gr2m

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?

garbados avatar May 07 '21 03:05 garbados