Giter VIP home page Giter VIP logo

2key-ratchet's Introduction

2key-ratchet

CircleCI License Coverage Status npm version

NPM

2key-ratchet is an implementation of a Double Ratchet protocol and X3DH in TypeScript utilizing WebCrypto.

The Double Ratchet protocol and X3DH were designed with goals of providing both forward secrecy and cryptographic deniability. Importantly there have been several independent security reviews that concluded they deliver on those goals.

The term “Double Ratchet” comes from how the protocol makes sure each message gets a new key: their Diffie-Hellman keys are “ratcheted” by each new message exchange; and so are the send/receive chains (the “symmetric-key ratchet”).

There are a few differences between the original specifications and 2key-ratchet, the most significant being, as it’s name suggests, it uses two keys, one for authentication and another for key exchange. The other big one is that secp256r1 is used instead of curve25519 because browsers do not yet support this curve natively.

See the ARCHITECTURE document to better understand the library structure.

For ideas on where you might use 2key-ratchet see the SCENARIOS document.

For licensing information, see the LICENSE file.

Overview

IdentityKeys

Each peer in the protocol has an IdentityKey, these are secp256r1 keys. These keys are used to authenticate both PreKeys and ExchangeKeys. IdentityKeys are used similarly to the public key in an X.509 certificate.

ExchangeKeys

ExchangeKeys are introduced by 2key-ratchet, they are used to derive PreKeys. The ExchangeKey is signed by a peers IdentityKey.

PreKeys

In 2key-ratchet a PreKey is a secp256r1 public key with an associated unique id. These PreKeys are signed by the IdentityKey.

On first use, clients generate a single signed PreKey, as well as a large list of unsigned PreKeys, and transmit all of them to a server.

Server

The server in the protocol is an untrusted entity, it simply stores preKeys for retrieval when the peer may be offline and unreachable.

Sessions

The Double Ratchet protocol is session-oriented. Peers establish a session with each other, this is then used for all subsequent exchanges. These sessions can remain open and be re-used since each message is encrypted with a new and unique cryptographic key.

Size and Dependencies

Name Size Description
2key-ratchet.js 66 Kb UMD module without external modules

NOTE: You will also have to import tslib and protobufjs for use in the browser.

Instructions

Installation

npm install 2key-ratchet

Usage

Include 2key-ratchet and its dependencies in your application.

NODEJS:

let DKeyRatchet = require("2key-ratchet");

BROWSER:

<script src="2key-ratchet.js"></script>

The DKeyRatchet namespace will always be available globally and also supports AMD loaders.

Generate an IdentityKey

The first step is to create an IdentityKey.

let AliceID;
DKeyRatchet.Identity.create(16453, 1, 1)
    .then((id) => {
        AliceID = id;
    });

Then create your PreKey message bundle:

let bundle = new DKeyRatchet.PreKeyBundleProtocol();

bundle.identity.fill(AliceID)
    .then(() => {
        bundle.registrationId = AliceID.id;
        const preKey = AliceID.signedPreKeys[0];
        bundle.preKeySigned.id = 1;
        bundle.preKeySigned.key = preKey.publicKey;
        return bundle.preKeySigned.sign(AliceID.signingKey.privateKey);
    })
    .then(() => {
        return bundle.exportProto();
    })
    .then((ab) => {
        console.log(ab); // ArrayBuffer { byteLength: 374 }
    });

And then import the generated PreKey message bundle:

DKeyRatchet.PreKeyBundleProtocol.importProto(ab)
    .then((bundle) => {
        // check signed prekey
        return bundle.preKeySigned.verify(AliceID.signingKey.publicKey);
    })
    .then((trusted) => {
        if (!trusted)
            throw new Error("Error: The PreKey is not trusted");
    })

Create a session

With the previous steps complete you can now create a session:

NOTE: For data conversion was used module pvtsutils.

DKeyRatchet.AsymmetricRatchet.create(BobID, bundle)
    .then((cipher) => {
        return cipher.encrypt(Convert.FromUtf8String("Hello world!"));
    })
    .then((preKeyMessage) => {
        return preKeyMessage.exportProto();
    })
    .then((BobMessage) => {
        console.log(BobMessage); // ArrayBuffer {byteLength: 408}
    });

On the other side you would do the same:

// Parse received bytes to proto
return DKeyRatchet.PreKeyMessageProtocol.importProto(BobMessage)
    .then((proto) => {
        return DKeyRatchet.AsymmetricRatchet.create(AliceID, proto)
            .then((cipher) => {
                return cipher.decrypt(proto.signedMessage);
            })
            .then((message) => {
                console.log(Convert.ToUtf8String(message)); // Hello world!
            });
    });

We have a complete example you can look at here.

Contributing

If you've found an problem with 2key-ratchet, please open a new issue. Feature requests are welcome, too.

Pull requests – patches, improvements, new features – are a fantastic help. Please ask first before embarking on any significant pull request (e.g., implementing new features).

Note

Bruce Schneier famously said "If you think cryptography can solve your problem, then you don't understand your problem and you don't understand cryptography". The point being, using 2key-ratchet, or any other "cryptography related" library, will not necessarily make your product secure.

In short, there is a lot more to making a secure product than adding cryptography, this is a great book to get you familiar with thinking defensively.

WARNING

Though this library is based on the Double Ratchet Algorithm and the X3DH Key Agreement Protocol several changes have been made that could change the security properties they offer. At this time you should consider this implementation appropriate for experimentation until further security reviews are completed.

Acknowledgements

Both Double Ratchet and X3DH were designed by Trevor Perrin and Moxie Marlinspike, we thank them for their work.

Related

2key-ratchet's People

Contributors

dependabot[bot] avatar microshine avatar rmhrisk avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

2key-ratchet's Issues

Json export not converting CryptoKey to object causing storage problems.

Hey, so been using this for a personal project, and it looks like when i use the toJSON available in Identity class, it's able to convert i believe most of the data but fails to convert the SigningKey which has the publicKey and privateKey which is of object CryptoKey.

When trying to store this on client side applications using Node JS, stringify'ing this causes data to go completely missing and end up like this.

"{"id":2,"signingKey":{"privateKey":{},"publicKey":{},"thumbprint":"fbc-----------------3fe3707ff5e97898d6----------f028ea128b"},"exchangeKey":{"privateKey":{},"publicKey":{},"thumbprint":"0ef4fa--------4751c82f1--------fe11b62020--------ddb60a"},"preKeys":[],"signedPreKeys":[{"privateKey":{},"publicKey":{},"thumbprint":"4f1c26----------0d0023db800dd547---------dd776eba682"}],"createdAt":"---------------"}"

Note: The hyphens are just blocking out data

Could the CryptoKey use the tsprotobuf to export, or is it already there and i'm not seeing it ?

Review `devDependencies` and move appropriate ones into `dependencies`

Right now all dependencies are in devDependencies

 "devDependencies": {
   "@types/chai": "^3.4.34",
   "@types/mocha": "^2.2.38",
   "@types/node": "^7.0.2",
   "@types/protobufjs": "^5.0.31",
   "mocha": "^3.2.0",
   "chai": "^3.5.0",
   "protobufjs": "^6.6.3",
   "tslib": "^1.5.0"
}

I suspect some should move to dependencies, for example:

  "dependencies": {
    "@types/protobufjs": "^5.0.31",
    "protobufjs": "^6.6.3",
    "tslib": "^1.5.0"
}

Example - "bad decrypt"

Hello! I'm running the simple example with one line added to set the crypto engine (full source below). I see (node:98107) UnhandledPromiseRejectionWarning: Error: error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt as a result. Any thoughts on what is wrong?


const DKeyRatchet = require("2key-ratchet");
const { Convert } = require("pvtsutils");

const { Crypto } = require("@peculiar/webcrypto");
const crypto = new Crypto();
DKeyRatchet.setEngine("@peculiar/webcrypto", crypto);

let AliceID, BobID;
let AlicePreKeyBundleProto;
let BobMessage;
Promise.resolve()
    .then(() => {
        // Create Alice's identity
        return DKeyRatchet.Identity.create(16453, 1, 1)
            .then((identity) => {
                AliceID = identity;

                // Create PreKeyBundle
                let AlicePreKeyBundle = new DKeyRatchet.PreKeyBundleProtocol();
                AlicePreKeyBundle.identity.fill(AliceID)
                    .then(() => {
                        AlicePreKeyBundle.registrationId = AliceID.id;
                        // Add info about signed PreKey
                        const preKey = AliceID.signedPreKeys[0];
                        AlicePreKeyBundle.preKeySigned.id = 0;
                        AlicePreKeyBundle.preKeySigned.key = preKey.publicKey;
                        return AlicePreKeyBundle.preKeySigned.sign(AliceID.signingKey.privateKey)
                            .then(() => {
                                // Convert proto to bytes
                                return AlicePreKeyBundle.exportProto();
                            })
                            .then((bytes) => {
                                AlicePreKeyBundleProto = bytes;
                                console.log("Alice's bundle: ", AlicePreKeyBundleProto);
                            });
                    })
            })
            .then(() => {
                // Create Bob's identity
                return DKeyRatchet.Identity.create(0, 1, 1)
                    .then((identity) => {
                        BobID = identity;

                        // Parse Alice's bundle
                        return DKeyRatchet.PreKeyBundleProtocol.importProto(AlicePreKeyBundleProto)
                            .then((bundle) => {
                                // Create Bob's cipher
                                return DKeyRatchet.AsymmetricRatchet.create(BobID, bundle);
                            })
                    })
                    .then((BobCipher) => {
                        // Encrypt message for Alice
                        return BobCipher.encrypt(Convert.FromUtf8String("Hello Alice!!!"));
                    })
                    .then((proto) => {
                        // convert message to bytes array
                        return proto.exportProto()
                    })
                    .then((bytes) => {
                        BobMessage = bytes;
                        console.log("Bob's encrypted message:", BobMessage);
                    })
            })
            .then(() => {
                // Decrypt message by Alice

                // Note: First message from Bob must be PreKeyMessage
                // parse Bob's message
                return DKeyRatchet.PreKeyMessageProtocol.importProto(BobMessage)
                    .then((proto) => {
                        // Creat Alice's cipher for Bob's message
                        return DKeyRatchet.AsymmetricRatchet.create(AliceID, proto)
                            .then((AliceCipher) => {
                                // Decrypt message
                                return AliceCipher.decrypt(proto.signedMessage);
                            })
                            .then((bytes) => {
                                console.log("Bob's decrypted message:", Convert.ToUtf8String(bytes));
                            })
                    });
            })

    })
    .catch((e) => {
        console.error(e);
    })

Update readme with example of specifying the dependencies

protobufjs has several options, one is minimal but its not clear if that works for us.

We should update the readme with examples of including the dependencies that the subsequent code would need to run so people do not need to guess which flavor of protobufjs is needed.

Any suggestions for a group message example?

Have been reading the Signal and WhatsApp docs on how they use X3DH to support group messaging. Just wondering - has there been any thought on using 2key-ratchet for that in it's current version?

Update growl version

A CVE has been reported with a dependency, the issue may not be relevant to 2key-ratchet but we should update.

Need to move to growl ~> 1.10.0

Related is: CVE-2017-16042

No known options for function setEngine

Using 2key-ratchet requires setting an engine which I couldn't find in the docs. What options are available to include as parameters?

Steps to Reproduce

  • Use this example.
  • This raises Error: WebCrypto engine is empty. Use setEngine to resolve it..
  • Set the engine DKeyRatchet.setEngine('WebCrypto')
  • This results in UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'subtle' of undefined which is due to the crypto attribute not being declared as far as I can tell.

Review exchange key usage and ensure no risk, or design mitigation for key replay.

One of the many benefits of 25519 is that you can use the same key for signing and encryption without risk.

The move to secp256r1 requires us to introduce an exchange key, which we sign with the identity key creating a cryptographic binding between the two.

This introduces a potential risk of an exchange key replay, we need to review this scenario and ensure that either there is no risk or we must design a mitigation to this

Add coveralls reporting

We are currently not publishing Coveralls reports, this is due to some problems on the coveralls service that prevent it from seeing this repository.

Add basic gh-pages demo page

We will want to show people that the project works, the easiest way to do this is by adding a basic demo page in the gh-pages branch.

Add total size, including dependencies to readme

Right now we show the size of the module itself but not its required dependencies, we should also include an approximate total size.

For example what does:

Add to the total package size?

The minimal prodobuf.js seems to add 6kb

While tslib.js seems to add 6.5k

If correct the total size to get the capabilities of this library it costs them 78.5Kb which isn't bad.

For users to make meaningful decisions on size this needs to be understood.

Need to implement a session store

We do not want each application to build this component, as such we should implement a session store that can be relied upon across browsers and node. We can possibly use IndexDB and a polyfill for node.

Add websocket example using library

One of the most common usages will be with WebSockets, lets make an example using websockets to make this easier for people to get started.

Using 'unset-eval' in Content Security Policy throws the error

Hi,
I'm using the 2key-ratchet library for end-to-end encryption in my React.js app.
After installing CSP on the website, I started facing issues with data encryption.
When trying to encrypt data, I'm getting an error asking me to add the "unsafe-eval" property to the CSP.
The application's security policy does not allow me to add the "unsafe-eval" property to the CSP.
Could you please fix this on your side or let me know how I can fix it on my side?

import * as DKeyRatchetSource from '2key-ratchet';

const DKeyRatchet = overwrite(DKeyRatchetSource);

// overwrite the package 
function overwrite(dependency) {
  const HASH_NAME = "SHA-256";
  const HMAC_NAME = "HMAC";

  dependency.Secret.importHMAC = function (raw) {
    return dependency.getEngine().crypto.subtle
      .importKey("raw", raw, { name: HMAC_NAME, hash: { name: HASH_NAME } }, true, ["sign", "verify"]);
  };

  return dependency;
}

// convert the encrypted message to a buffer and create a protocol instance from it
export async function processMessage(message) {
  const messageRaw = convertBase64ToBuffer(message);

  let messageEncrypted = await DKeyRatchet.MessageSignedProtocol.importProto(messageRaw).catch(() => { });

  if (!messageEncrypted) {
    messageEncrypted = await DKeyRatchet.PreKeyMessageProtocol.importProto(messageRaw); // Error in this method
  }

  return messageEncrypted;
}


Screenshot

Need to implement a trust store

We do not want to have every application deal with securely storing trusted keys and chains, as such, we should provide an implementation that they can all use. Possibly using IndexDB with a polyfill on Node.

Persisting cipher object

Is there an easier way to persist the cipher object (DKeyRatchet.AsymetricRatchet) ?
I tried serializing with toJson() and hydrating with fromJson(), but there seems to be a bug in the implementation for cipher generated from PreKeyBundleProtocol as there are some fields that is not properly hydrated.

import * as DKeyRatchet from "..";
import {Convert} from "pvtsutils";
import {Crypto} from "@peculiar/webcrypto";
import * as _ from 'lodash';


function getObjectDiff(obj1: any, obj2:any) {
    const diff = Object.keys(obj1).reduce((result, key) => {
        if (!obj2.hasOwnProperty(key)) {
            result.push(key);
        } else if (_.isEqual(obj1[key], obj2[key])) {
            const resultKeyIndex = result.indexOf(key);
            result.splice(resultKeyIndex, 1);
        }
        return result;
    }, Object.keys(obj2));
    return diff;
}

async function compareCiphers(cipher: DKeyRatchet.AsymmetricRatchet) {
    const cipherJson = await cipher.toJSON();
    const identity = cipher.identity;
    const remoteIdentity = cipher.remoteIdentity;
    const restoredCipher = await DKeyRatchet.AsymmetricRatchet.fromJSON(identity, remoteIdentity, cipherJson);
    console.log(_.isEqual(cipher, restoredCipher));
    console.log(getObjectDiff(cipher, restoredCipher));
    console.log('cipher: ', cipher.currentStep);
    console.log('restoredCipher: ', restoredCipher.currentStep);
}

async function main() {
    const crypto = new Crypto();
    DKeyRatchet.setEngine("@peculiar/webcrypto", crypto);

    const time = new Date().getTime();
    const aliceID = await DKeyRatchet.Identity.create(time, 1);
    const alicePreKeyBundle = new DKeyRatchet.PreKeyBundleProtocol();
    await alicePreKeyBundle.identity.fill(aliceID);
    alicePreKeyBundle.registrationId = aliceID.id;
    const preKey = aliceID.signedPreKeys[0];
    alicePreKeyBundle.preKeySigned.id = 0;
    alicePreKeyBundle.preKeySigned.key = preKey.publicKey;
    await alicePreKeyBundle.preKeySigned.sign(aliceID.signingKey.privateKey);

    // Bob sending encrypted message to Alice
    const bobID = await DKeyRatchet.Identity.create(2000, 1);
    const aliceBundle = await DKeyRatchet.PreKeyBundleProtocol.importProto(alicePreKeyBundle);
    const bobCipher = await DKeyRatchet.AsymmetricRatchet.create(bobID, aliceBundle);
    const bobEncryptedMessage = await bobCipher.encrypt(Convert.FromUtf8String('Hello Alice from Bob'));
    console.log('bobCipher');
    await compareCiphers(bobCipher);
}

main().catch((e) => console.error(e));

The result:

bobCipher
false
[ 'currentStep', 'remotePreKeyId', 'remotePreKeySignedId', 'id' ]
cipher: DHRatchetStep {
remoteRatchetKey: ECPublicKey {
key: CryptoKey {
algorithm: [Object],
type: 'public',
extractable: true,
usages: []
},
serialized: ArrayBuffer {
[Uint8Contents]: <76 bf 7e 3f fd 9c 2b 26 5a b9 0b 9e 02 e0 7a 3e e3 aa 36 e7 5a 48 39 f4 bc ea 82 53 7f a8 f6 52 13 03 1b
23 03 b6 e5 d8 b4 25 ed 8f 2f e6 88 c7 93 0d 97 00 3b e4 af 85 51 fe b4 a5 65 ce 47 3f>,
byteLength: 64
},
id: 'd1e3669b7d3b03567e57171c50435049424996f9b5d5c5a30bcd92bdc8a9c01f'
},
sendingChain: SendingRatchet {
counter: 1,
rootKey: CryptoKey {
algorithm: [Object],
type: 'secret',
extractable: false,
usages: [Array]
}
}
}
restoredCipher: DHRatchetStep {}

createPreKeyBundle helper is needed

Example

protected async randomBundle() {
    const preKeyBundle = new PreKeyBundleProtocol();
    await preKeyBundle.identity.fill(this.identity);
    const preKeyId = getRandomInt(1, this.identity.signedPreKeys.length).toString();
    preKeyBundle.preKeySigned.key = this.identity.preKeys.load(preKeyId.toString()).key.publicKey;
    await preKeyBundle.preKeySigned.sign(this.identity.signingKey.privateKey);
    return preKeyBundle.exportProto();
}

envelope routines:EVP_DecryptFinal_ex:bad decrypt when using of one time pre key test.

Looking at the test for the usage of one time pre key, i found this,

`

async function Test() {
    const identity = await createIdentity(1);
    const bundle = await createPreKeyBundle(identity);
    bundle.preKey.id = 1;
    bundle.preKey.key = bundle.preKeySigned.key;
    const raw = await bundle.exportProto();
    const bundle2 = await PreKeyBundleProtocol.importProto(raw);
    assert.isFalse(bundle2.preKey.isEmpty());
    assert.isFalse(bundle2.preKeySigned.isEmpty());
    assert.isFalse(bundle2.identity.isEmpty());
}

`

Although this works, the bundle does get exported and imported correctly with the prekey present, using it in the server example provided, and running the server-client example yields, this error.

envelope routines:EVP_DecryptFinal_ex:bad decrypt:../deps/openssl/openssl/crypto/evp/evp_enc.c`

Looking around, i found that this happens when the key provided is wrong and it's not able to decrypt but i don't think it's the case here. Any suggestions as to why this could be happening?

Support asynchronous messaging

Right now 2key presumes an ordered series of messages, this is problematic in a few cases.

For example when a client is invoked with promises you dont know in which sequence things will happen.

2key needs to make sure it uses the right key for the right message so that when invoked with promise.all, as an example, it uses the right key for each message.

Simplify calling semantics

Right now the caller needs to do a few things, to establish their session with a peer, in an ideal world these steps would be taken care of by a higher level construct so there are fewer things to go wrong.

We should think about how we can simplify things for the caller.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.