PeculiarVentures/2key-ratchet

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

Go

126

174 commits

updated Apr 1, 2026

See the code

README

2key-ratchet

[!WARNING] This library is no longer actively maintained. It is kept available for wire-compatibility reference and to support existing deployed Fortify clients during migration. No new features or security fixes will be made.

New projects should use pqc-ratchet, which provides the same Double Ratchet + X3DH protocol with post-quantum security (ML-KEM-768 + ML-DSA-65) and a significantly simpler API. A Go server implementation for migrating off the TypeScript client without breaking wire compatibility is available in the go/ directory.

CircleCI License Coverage Status npm version

NPM

▶ Interactive Browser Demo — runs entirely in your browser, no server required.

2key-ratchet is an implementation of the Double Ratchet protocol and X3DH key agreement, available in both TypeScript (using WebCrypto) and Go. The two implementations are byte-level compatible and validated against each other through cross-language interop tests.

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

TypeScript

NameSizeDescription
2key-ratchet.js66 KbUMD module without external modules

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

Go

NameSizeDescription
go/ (source)101 KbProtocol, wire format, server, and action dispatch
go/ (test)62 KbUnit, interop, and live integration tests

Dependencies: golang.org/x/crypto (HKDF) and github.com/gorilla/websocket (WebSocket server). No CGo, no protobuf code generation.

TypeScript

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.

Go

A full Go implementation lives in the go/ directory, including the ratchet protocol, wire format, webcrypto-socket server, and webcrypto-local action dispatch with the CryptoProvider interface.

Installation

go get github.com/PeculiarVentures/2key-ratchet/go

Usage

import ratchet "github.com/PeculiarVentures/2key-ratchet/go"

// Generate an identity
identity, _ := ratchet.GenerateIdentity(1, 10, 0)

// Start the webcrypto-socket server
server, _ := ratchet.NewWebCryptoServer(ratchet.ServerConfig{
    Address:        "127.0.0.1:31337",
    TLSCert:        tlsCert,
    Identity:       identity,
    CryptoProvider: myCryptoProvider, // implements ratchet.CryptoProvider
    OnChallenge: func(pin, origin string) bool {
        return showPINDialog(pin, origin)
    },
})
server.ListenAndServe(ctx)

The Go implementation is byte-level compatible with TypeScript v1.0.18 and validated through 43 tests, including cross-language interop tests and live integration tests against the TS client SDK. See go/README.md for the full API reference, known limitations, and protocol compatibility notes.

Interop testing

The interop/ directory contains a Node.js test harness that generates cross-language test vectors and runs the TS client SDK against the Go server. Both implementations produce identical bytes at every protocol step.

cd interop && npm install && cd ../go && go test -v ./...

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.

cryptography
diffie-hellman
ecc
encryption
integrity
javascript
privacy
secp256r1
secrecy
session
typescript
webcrypto

Contributors

rmhrisk

98 commits

microshine

73 commits

PeculiarVentures/2key-ratchet

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

Go

126

174 commits

updated Apr 1, 2026

See the code

README

2key-ratchet

[!WARNING] This library is no longer actively maintained. It is kept available for wire-compatibility reference and to support existing deployed Fortify clients during migration. No new features or security fixes will be made.

New projects should use pqc-ratchet, which provides the same Double Ratchet + X3DH protocol with post-quantum security (ML-KEM-768 + ML-DSA-65) and a significantly simpler API. A Go server implementation for migrating off the TypeScript client without breaking wire compatibility is available in the go/ directory.

CircleCI License Coverage Status npm version

NPM

▶ Interactive Browser Demo — runs entirely in your browser, no server required.

2key-ratchet is an implementation of the Double Ratchet protocol and X3DH key agreement, available in both TypeScript (using WebCrypto) and Go. The two implementations are byte-level compatible and validated against each other through cross-language interop tests.

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

TypeScript

NameSizeDescription
2key-ratchet.js66 KbUMD module without external modules

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

Go

NameSizeDescription
go/ (source)101 KbProtocol, wire format, server, and action dispatch
go/ (test)62 KbUnit, interop, and live integration tests

Dependencies: golang.org/x/crypto (HKDF) and github.com/gorilla/websocket (WebSocket server). No CGo, no protobuf code generation.

TypeScript

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.

Go

A full Go implementation lives in the go/ directory, including the ratchet protocol, wire format, webcrypto-socket server, and webcrypto-local action dispatch with the CryptoProvider interface.

Installation

go get github.com/PeculiarVentures/2key-ratchet/go

Usage

import ratchet "github.com/PeculiarVentures/2key-ratchet/go"

// Generate an identity
identity, _ := ratchet.GenerateIdentity(1, 10, 0)

// Start the webcrypto-socket server
server, _ := ratchet.NewWebCryptoServer(ratchet.ServerConfig{
    Address:        "127.0.0.1:31337",
    TLSCert:        tlsCert,
    Identity:       identity,
    CryptoProvider: myCryptoProvider, // implements ratchet.CryptoProvider
    OnChallenge: func(pin, origin string) bool {
        return showPINDialog(pin, origin)
    },
})
server.ListenAndServe(ctx)

The Go implementation is byte-level compatible with TypeScript v1.0.18 and validated through 43 tests, including cross-language interop tests and live integration tests against the TS client SDK. See go/README.md for the full API reference, known limitations, and protocol compatibility notes.

Interop testing

The interop/ directory contains a Node.js test harness that generates cross-language test vectors and runs the TS client SDK against the Go server. Both implementations produce identical bytes at every protocol step.

cd interop && npm install && cd ../go && go test -v ./...

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.

cryptography
diffie-hellman
ecc
encryption
integrity
javascript
privacy
secp256r1
secrecy
session
typescript
webcrypto

Contributors

rmhrisk

98 commits

microshine

73 commits

Languages

Go

59.2%

TypeScript

29.9%

JavaScript

10.9%