@bicycle-codes/envelope
TypeScript icon, indicating that this package has built-in type declarations

0.3.3 • Public • Published

envelope

tests Socket Badge module types Common Changelog semantic versioning install size license

Envelopes that have been authorized by the recipient. This hides the sender's identity, while the recipient is still visible. This way we hide the metadata of who is talking to whom via private message. But, because the recipient is legible, we can still index messages by recipient.

This supports multiple devices by default because we are using the Identity module.

Each envelope includes a signature. We want to give out our signed envelopes privately, without revealing them publicly.

If we assume we are doing this with internet infrastructure (ie, a server), then in the initial meeting the server would be able to see who we are giving out certificates to because the recipient must be visible. But in subsequent communication, the server would not know who we are talking to, they would just know that we are communicating with someone that we have given a certificate to.

You could also give out the certificates via some other means, like on your website, or via signal message, in which case the server would not know who it is from. Meaning, the server cannot even assume that a message is from your 'friend circle' in the application. It can only see that you got a message at a particular time; we can't infer anything about who it is from.

This is assuming that all the users of the app are well behaved, and not giving out envelopes willy nilly. 🤔

contents

metaphors

If we stick with comparisons to common physical activities, this is very similar to the postal service. The envelope shows the recipient, and it needs a stamp (the signature here), but it hides the sender's ID.

Identity

The envelopes and encrypted messages pair with an identity instance instance on your device.

We create a symmetric key and encrypt it to various "exchange" keys. The exchange keys are non-extractable key pairs that can only be used on the device where they were created.

That way the documents created by this library can be freely distributed without leaking any keys.

an envelope

Just a document signed by the recipient, like this:

// envelope
{
    seq: 0,
    expiration: 456,
    recipient: 'my-username',
    signature: '123abc',
    author: 'did:key:abc'
}

a message in an envelope

// the message
{ envelope, content: 'encrypted text' }
// sender ID is in the content, so it is only readable by
//   the recipient

types

Envelope

import type { SignedMessage } from '@bicycle-codes/message'

type Envelope = SignedMessage<{
    seq:number,
    expiration?:number,  // default to 0, which means no expiration
    recipient:string,  // the recipient's username
}>

EncryptedContent

When you encrypt a string, we create a record of keys. The key object is a map from device name to a symmetric key that has been encrypted to the device. We do it this way because each device has its own keypair. We use the symmetric key to encrypt the content.

interface EncryptedContent {
    key:Record<string, string>,  // { deviceName: 'encrypted-key' }
    content:string  // encrypted text
}

API

create

Create an envelope.

async function create (
    // crypto:Implementation,
    signingKeypair:CryptoKeyPair,
    {
        username,
        seq,
        expiration = 0  // no expiration by default
    }:{ username:string, seq:number, expiration?:number }
):Promise<Envelope>

wrapMessage

Create a new AES key, take an envelope and some content. Encrypt the content, then put the content in the envelope.

This will encrypt the AES key to every device in the recipient identity, as well as your own identity.

import { Identity } from '@bicycle-codes/identity'

async function wrapMessage (
    me:Identity,
    recipient:Identity,  // because we need to encrypt the message to the recipient
    envelope:Envelope,
    content:Content
):Promise<[{
    envelope:Envelope,
    message:EncryptedContent
}, Keys]>

This returns an array of

[{ envelope, message: encryptedMessage }, { ...senderKeys }]

[!NOTE] We return the sender keys as a seperate object because we do not want the sender's device names to be in the message that gets sent, because that would leak information about who the sender is.

The sender could save a map of the message's hash to the returned key object. That way they can save the map to some storage, and then look up the key by the hash of the message object.

decryptMessage

Decrypt a given message. Depends on having the right crypto object. Return a Content object:

type Content = SignedRequest<{
    from:{ username:string },
    text:string,
    mentions?:string[],
}>

export async function decryptMessage (
    crypto:Crypto.Implementation,
    msg:EncryptedContent
):Promise<Content>

example

import { decryptMessage } from '@bicycle-codes/envelope'

const decrypted = await decryptMessage(alicesCrypto, msgContent)

console.log(decrypted.from.username)
// => bob
console.log(decrypted.text)
// => hello

Decrypt a message that I wrote

Pass in the keys as a separate argument if you are the message author. The sender's keys are not in the message evnelope, because we need to keep your device names out of the unencrypted envelope.

import { decryptMessage } from '@bicycle-codes/envelope'

// bobs keys were not in the envelope, because doing so would
// reveal information about the message author, Bob.
const decrypted = await decryptMessage(bob, msgContent, bobsMsgKeys)

console.log(decrypted.from.username)
// => bob

console.log(decrypted.text)
// => hello

verify

Check if a given envelope is valid. currentSeq is an optional sequence number to use when checking the validity. If currentSeq is less than or equal to seq in the envelope, then this will return false.

function verify (envelope:Envelope, currentSeq?:number):Promise<boolean>
test('check that the envelope is valid', async t => {
    const isValid = await verify(alicesEnvelope)
    t.equal(isValid, true, 'should validate a valid envelope')

    t.equal(await verify(alicesEnvelope, 0), true,
        'should take a sequence number')

    t.equal(await verify(alicesEnvelope, 1), false,
        'should say a message is invalid if the sequence number is equal')

    try {
        t.equal(await verify('baloney'))
    } catch (err) {
        t.ok(err, 'should throw given a malformed message')
    }
})

Thanks to @Dominic for sketching this idea originally.

Package Sidebar

Install

npm i @bicycle-codes/envelope

Weekly Downloads

13

Version

0.3.3

License

SEE LICENSE IN LICENSE

Unpacked Size

35.3 kB

Total Files

8

Last publish

Collaborators

  • nichoth