Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

getAttachment doesn't work #13

Open
mikeymckay opened this issue Nov 4, 2015 · 17 comments
Open

getAttachment doesn't work #13

mikeymckay opened this issue Nov 4, 2015 · 17 comments

Comments

@mikeymckay
Copy link
Contributor

Steps to reproduce:

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

--> CustomPouchError

@calvinmetcalf
Copy link
Owner

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

@gr2m
Copy link
Collaborator

gr2m commented Feb 12, 2016

I’m looking into this :)

@gr2m
Copy link
Collaborator

gr2m commented Feb 12, 2016

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
Copy link
Collaborator

gr2m commented Feb 12, 2016

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

@nolanlawson
Copy link
Contributor

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
Copy link
Contributor

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

@gr2m
Copy link
Collaborator

gr2m commented Feb 18, 2016

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?

@xMartin
Copy link

xMartin commented Feb 19, 2016

The workaround returns base64 while getAttachment returns a blob.

@gr2m
Copy link
Collaborator

gr2m commented Feb 19, 2016

sorry, got the options wrong above, instead of

    attachments: {
      binary: true
    }

it's

    attachments: true,
    binary: true

I’ve updated the code above

@gr2m
Copy link
Collaborator

gr2m commented Feb 19, 2016

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

screen shot 2016-02-19 at 14 04 35

@xMartin
Copy link

xMartin commented Feb 19, 2016

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

@xMartin
Copy link

xMartin commented Feb 19, 2016

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

@gr2m
Copy link
Collaborator

gr2m commented Feb 19, 2016

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
Copy link
Collaborator

gr2m commented Feb 19, 2016

this works around the issue for node: #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
Copy link
Collaborator

gr2m commented Apr 5, 2016

2nd attempt: #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
Copy link
Collaborator

gr2m commented May 10, 2016

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

@garbados
Copy link
Collaborator

garbados commented May 7, 2021

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants