Giter VIP home page Giter VIP logo

openpgpjs's Introduction

OpenPGP.js BrowserStack Status Join the chat on Gitter

OpenPGP.js is a JavaScript implementation of the OpenPGP protocol. It implements RFC4880 and parts of RFC4880bis.

Table of Contents

Platform Support

  • The dist/openpgp.min.js bundle works well with recent versions of Chrome, Firefox, Safari and Edge.

  • The dist/node/openpgp.min.js bundle works well in Node.js. It is used by default when you require('openpgp') in Node.js.

  • Currently, Chrome, Safari and Edge have partial implementations of the Streams specification, and Firefox has a partial implementation behind feature flags. Chrome is the only browser that implements TransformStreams, which we need, so we include a polyfill for all other browsers. Please note that in those browsers, the global ReadableStream property gets overwritten with the polyfill version if it exists. In some edge cases, you might need to use the native ReadableStream (for example when using it to create a Response object), in which case you should store a reference to it before loading OpenPGP.js. There is also the web-streams-adapter library to convert back and forth between them.

Performance

  • Version 3.0.0 of the library introduces support for public-key cryptography using elliptic curves. We use native implementations on browsers and Node.js when available. Elliptic curve cryptography provides stronger security per bits of key, which allows for much faster operations. Currently the following curves are supported:

    Curve Encryption Signature NodeCrypto WebCrypto Constant-Time
    curve25519 ECDH N/A No No Algorithmically**
    ed25519 N/A EdDSA No No Algorithmically**
    p256 ECDH ECDSA Yes* Yes* If native***
    p384 ECDH ECDSA Yes* Yes* If native***
    p521 ECDH ECDSA Yes* Yes* If native***
    brainpoolP256r1 ECDH ECDSA Yes* No If native***
    brainpoolP384r1 ECDH ECDSA Yes* No If native***
    brainpoolP512r1 ECDH ECDSA Yes* No If native***
    secp256k1 ECDH ECDSA Yes* No If native***

    * when available
    ** the curve25519 and ed25519 implementations are algorithmically constant-time, but may not be constant-time after optimizations of the JavaScript compiler
    *** these curves are only constant-time if the underlying native implementation is available and constant-time

  • Version 2.x of the library has been built from the ground up with Uint8Arrays. This allows for much better performance and memory usage than strings.

  • If the user's browser supports native WebCrypto via the window.crypto.subtle API, this will be used. Under Node.js the native crypto module is used.

  • The library implements the RFC4880bis proposal for authenticated encryption using native AES-EAX, OCB, or GCM. This makes symmetric encryption up to 30x faster on supported platforms. Since the specification has not been finalized and other OpenPGP implementations haven't adopted it yet, the feature is currently behind a flag. Note: activating this setting can break compatibility with other OpenPGP implementations, and also with future versions of OpenPGP.js. Don't use it with messages you want to store on disk or in a database. You can enable it by setting openpgp.config.aeadProtect = true.

    You can change the AEAD mode by setting one of the following options:

    openpgp.config.preferredAEADAlgorithm = openpgp.enums.aead.eax // Default, native
    openpgp.config.preferredAEADAlgorithm = openpgp.enums.aead.ocb // Non-native
    openpgp.config.preferredAEADAlgorithm = openpgp.enums.aead.experimentalGCM // **Non-standard**, fastest
    
  • For environments that don't provide native crypto, the library falls back to asm.js implementations of AES, SHA-1, and SHA-256.

Getting started

Node.js

Install OpenPGP.js using npm and save it in your dependencies:

npm install --save openpgp

And import it as a CommonJS module:

const openpgp = require('openpgp');

Or as an ES6 module, from an .mjs file:

import * as openpgp from 'openpgp';

Deno (experimental)

Import as an ES6 module, using /dist/openpgp.mjs.

import * as openpgp from './openpgpjs/dist/openpgp.mjs';

Browser (webpack)

Install OpenPGP.js using npm and save it in your devDependencies:

npm install --save-dev openpgp

And import it as an ES6 module:

import * as openpgp from 'openpgp';

You can also only import the functions you need, as follows:

import { readMessage, decrypt } from 'openpgp';

Or, if you want to use the lightweight build (which is smaller, and lazily loads non-default curves on demand):

import * as openpgp from 'openpgp/lightweight';

To test whether the lazy loading works, try to generate a key with a non-standard curve:

import { generateKey } from 'openpgp/lightweight';
await generateKey({ curve: 'brainpoolP512r1',  userIDs: [{ name: 'Test', email: '[email protected]' }] });

For more examples of how to generate a key, see Generate new key pair. It is recommended to use curve25519 instead of brainpoolP512r1 by default.

Browser (plain files)

Grab openpgp.min.js from unpkg.com/openpgp/dist, and load it in a script tag:

<script src="openpgp.min.js"></script>

Or, to load OpenPGP.js as an ES6 module, grab openpgp.min.mjs from unpkg.com/openpgp/dist, and import it as follows:

<script type="module">
import * as openpgp from './openpgp.min.mjs';
</script>

To offload cryptographic operations off the main thread, you can implement a Web Worker in your application and load OpenPGP.js from there. For an example Worker implementation, see test/worker/worker_example.js.

TypeScript

Since TS is not fully integrated in the library, TS-only dependencies are currently listed as devDependencies, so to compile the project you’ll need to add @openpgp/web-stream-tools manually (NB: only versions below v0.12 are compatible with OpenPGP.js v5):

npm install --save-dev @openpgp/[email protected]

If you notice missing or incorrect type definitions, feel free to open a PR.

Examples

Here are some examples of how to use OpenPGP.js v5. For more elaborate examples and working code, please check out the public API unit tests. If you're upgrading from v4 it might help to check out the changelog and documentation.

Encrypt and decrypt Uint8Array data with a password

Encryption will use the algorithm specified in config.preferredSymmetricAlgorithm (defaults to aes256), and decryption will use the algorithm used for encryption.

(async () => {
    const message = await openpgp.createMessage({ binary: new Uint8Array([0x01, 0x01, 0x01]) });
    const encrypted = await openpgp.encrypt({
        message, // input as Message object
        passwords: ['secret stuff'], // multiple passwords possible
        format: 'binary' // don't ASCII armor (for Uint8Array output)
    });
    console.log(encrypted); // Uint8Array

    const encryptedMessage = await openpgp.readMessage({
        binaryMessage: encrypted // parse encrypted bytes
    });
    const { data: decrypted } = await openpgp.decrypt({
        message: encryptedMessage,
        passwords: ['secret stuff'], // decrypt with password
        format: 'binary' // output as Uint8Array
    });
    console.log(decrypted); // Uint8Array([0x01, 0x01, 0x01])
})();

Encrypt and decrypt String data with PGP keys

Encryption will use the algorithm preferred by the public (encryption) key (defaults to aes256 for keys generated in OpenPGP.js), and decryption will use the algorithm used for encryption.

const openpgp = require('openpgp'); // use as CommonJS, AMD, ES6 module or via window.openpgp

(async () => {
    // put keys in backtick (``) to avoid errors caused by spaces or tabs
    const publicKeyArmored = `-----BEGIN PGP PUBLIC KEY BLOCK-----
...
-----END PGP PUBLIC KEY BLOCK-----`;
    const privateKeyArmored = `-----BEGIN PGP PRIVATE KEY BLOCK-----
...
-----END PGP PRIVATE KEY BLOCK-----`; // encrypted private key
    const passphrase = `yourPassphrase`; // what the private key is encrypted with

    const publicKey = await openpgp.readKey({ armoredKey: publicKeyArmored });

    const privateKey = await openpgp.decryptKey({
        privateKey: await openpgp.readPrivateKey({ armoredKey: privateKeyArmored }),
        passphrase
    });

    const encrypted = await openpgp.encrypt({
        message: await openpgp.createMessage({ text: 'Hello, World!' }), // input as Message object
        encryptionKeys: publicKey,
        signingKeys: privateKey // optional
    });
    console.log(encrypted); // '-----BEGIN PGP MESSAGE ... END PGP MESSAGE-----'

    const message = await openpgp.readMessage({
        armoredMessage: encrypted // parse armored message
    });
    const { data: decrypted, signatures } = await openpgp.decrypt({
        message,
        verificationKeys: publicKey, // optional
        decryptionKeys: privateKey
    });
    console.log(decrypted); // 'Hello, World!'
    // check signature validity (signed messages only)
    try {
        await signatures[0].verified; // throws on invalid signature
        console.log('Signature is valid');
    } catch (e) {
        throw new Error('Signature could not be verified: ' + e.message);
    }
})();

Encrypt to multiple public keys:

(async () => {
    const publicKeysArmored = [
        `-----BEGIN PGP PUBLIC KEY BLOCK-----
...
-----END PGP PUBLIC KEY BLOCK-----`,
        `-----BEGIN PGP PUBLIC KEY BLOCK-----
...
-----END PGP PUBLIC KEY BLOCK-----`
    ];
    const privateKeyArmored = `-----BEGIN PGP PRIVATE KEY BLOCK-----
...
-----END PGP PRIVATE KEY BLOCK-----`;    // encrypted private key
    const passphrase = `yourPassphrase`; // what the private key is encrypted with
    const plaintext = 'Hello, World!';

    const publicKeys = await Promise.all(publicKeysArmored.map(armoredKey => openpgp.readKey({ armoredKey })));

    const privateKey = await openpgp.decryptKey({
        privateKey: await openpgp.readKey({ armoredKey: privateKeyArmored }),
        passphrase
    });

    const message = await openpgp.createMessage({ text: plaintext });
    const encrypted = await openpgp.encrypt({
        message, // input as Message object
        encryptionKeys: publicKeys,
        signingKeys: privateKey // optional
    });
    console.log(encrypted); // '-----BEGIN PGP MESSAGE ... END PGP MESSAGE-----'
})();

If you expect an encrypted message to be signed with one of the public keys you have, and do not want to trust the decrypted data otherwise, you can pass the decryption option expectSigned = true, so that the decryption operation will fail if no valid signature is found:

(async () => {
    // put keys in backtick (``) to avoid errors caused by spaces or tabs
    const publicKeyArmored = `-----BEGIN PGP PUBLIC KEY BLOCK-----
...
-----END PGP PUBLIC KEY BLOCK-----`;
    const privateKeyArmored = `-----BEGIN PGP PRIVATE KEY BLOCK-----
...
-----END PGP PRIVATE KEY BLOCK-----`; // encrypted private key
    const passphrase = `yourPassphrase`; // what the private key is encrypted with

    const publicKey = await openpgp.readKey({ armoredKey: publicKeyArmored });

    const privateKey = await openpgp.decryptKey({
        privateKey: await openpgp.readPrivateKey({ armoredKey: privateKeyArmored }),
        passphrase
    });

    const encryptedAndSignedMessage = `-----BEGIN PGP MESSAGE-----
...
-----END PGP MESSAGE-----`;

    const message = await openpgp.readMessage({
        armoredMessage: encryptedAndSignedMessage // parse armored message
    });
    // decryption will fail if all signatures are invalid or missing
    const { data: decrypted, signatures } = await openpgp.decrypt({
        message,
        decryptionKeys: privateKey,
        expectSigned: true,
        verificationKeys: publicKey, // mandatory with expectSigned=true
    });
    console.log(decrypted); // 'Hello, World!'
})();

Encrypt symmetrically with compression

By default, encrypt will not use any compression when encrypting symmetrically only (i.e. when no encryptionKeys are given). It's possible to change that behaviour by enabling compression through the config, either for the single encryption:

(async () => {
    const message = await openpgp.createMessage({ binary: new Uint8Array([0x01, 0x02, 0x03]) }); // or createMessage({ text: 'string' })
    const encrypted = await openpgp.encrypt({
        message,
        passwords: ['secret stuff'], // multiple passwords possible
        config: { preferredCompressionAlgorithm: openpgp.enums.compression.zlib } // compress the data with zlib
    });
})();

or by changing the default global configuration:

openpgp.config.preferredCompressionAlgorithm = openpgp.enums.compression.zlib

Where the value can be any of:

  • openpgp.enums.compression.zip
  • openpgp.enums.compression.zlib
  • openpgp.enums.compression.uncompressed (default)

Streaming encrypt Uint8Array data with a password

(async () => {
    const readableStream = new ReadableStream({
        start(controller) {
            controller.enqueue(new Uint8Array([0x01, 0x02, 0x03]));
            controller.close();
        }
    });

    const message = await openpgp.createMessage({ binary: readableStream });
    const encrypted = await openpgp.encrypt({
        message, // input as Message object
        passwords: ['secret stuff'], // multiple passwords possible
        format: 'binary' // don't ASCII armor (for Uint8Array output)
    });
    console.log(encrypted); // raw encrypted packets as ReadableStream<Uint8Array>

    // Either pipe the above stream somewhere, pass it to another function,
    // or read it manually as follows:
    for await (const chunk of encrypted) {
        console.log('new chunk:', chunk); // Uint8Array
    }
})();

For more information on using ReadableStreams, see the MDN Documentation on the Streams API.

You can also pass a Node.js Readable stream, in which case OpenPGP.js will return a Node.js Readable stream as well, which you can .pipe() to a Writable stream, for example.

Streaming encrypt and decrypt String data with PGP keys

(async () => {
    const publicKeyArmored = `-----BEGIN PGP PUBLIC KEY BLOCK-----
...
-----END PGP PUBLIC KEY BLOCK-----`; // Public key
    const privateKeyArmored = `-----BEGIN PGP PRIVATE KEY BLOCK-----
...
-----END PGP PRIVATE KEY BLOCK-----`; // Encrypted private key
    const passphrase = `yourPassphrase`; // Password that private key is encrypted with

    const publicKey = await openpgp.readKey({ armoredKey: publicKeyArmored });

    const privateKey = await openpgp.decryptKey({
        privateKey: await openpgp.readPrivateKey({ armoredKey: privateKeyArmored }),
        passphrase
    });

    const readableStream = new ReadableStream({
        start(controller) {
            controller.enqueue('Hello, world!');
            controller.close();
        }
    });

    const encrypted = await openpgp.encrypt({
        message: await openpgp.createMessage({ text: readableStream }), // input as Message object
        encryptionKeys: publicKey,
        signingKeys: privateKey // optional
    });
    console.log(encrypted); // ReadableStream containing '-----BEGIN PGP MESSAGE ... END PGP MESSAGE-----'

    const message = await openpgp.readMessage({
        armoredMessage: encrypted // parse armored message
    });
    const decrypted = await openpgp.decrypt({
        message,
        verificationKeys: publicKey, // optional
        decryptionKeys: privateKey
    });
    const chunks = [];
    for await (const chunk of decrypted.data) {
        chunks.push(chunk);
    }
    const plaintext = chunks.join('');
    console.log(plaintext); // 'Hello, World!'
})();

Generate new key pair

ECC keys (smaller and faster to generate):

Possible values for curve are: curve25519, ed25519, p256, p384, p521, brainpoolP256r1, brainpoolP384r1, brainpoolP512r1, and secp256k1. Note that both the curve25519 and ed25519 options generate a primary key for signing using Ed25519 and a subkey for encryption using Curve25519.

(async () => {
    const { privateKey, publicKey, revocationCertificate } = await openpgp.generateKey({
        type: 'ecc', // Type of the key, defaults to ECC
        curve: 'curve25519', // ECC curve name, defaults to curve25519
        userIDs: [{ name: 'Jon Smith', email: '[email protected]' }], // you can pass multiple user IDs
        passphrase: 'super long and hard to guess secret', // protects the private key
        format: 'armored' // output key format, defaults to 'armored' (other options: 'binary' or 'object')
    });

    console.log(privateKey);     // '-----BEGIN PGP PRIVATE KEY BLOCK ... '
    console.log(publicKey);      // '-----BEGIN PGP PUBLIC KEY BLOCK ... '
    console.log(revocationCertificate); // '-----BEGIN PGP PUBLIC KEY BLOCK ... '
})();

RSA keys (increased compatibility):

(async () => {
    const { privateKey, publicKey } = await openpgp.generateKey({
        type: 'rsa', // Type of the key
        rsaBits: 4096, // RSA key size (defaults to 4096 bits)
        userIDs: [{ name: 'Jon Smith', email: '[email protected]' }], // you can pass multiple user IDs
        passphrase: 'super long and hard to guess secret' // protects the private key
    });
})();

Revoke a key

Using a revocation certificate:

(async () => {
    const { publicKey: revokedKeyArmored } = await openpgp.revokeKey({
        key: await openpgp.readKey({ armoredKey: publicKeyArmored }),
        revocationCertificate,
        format: 'armored' // output armored keys
    });
    console.log(revokedKeyArmored); // '-----BEGIN PGP PUBLIC KEY BLOCK ... '
})();

Using the private key:

(async () => {
    const { publicKey: revokedKeyArmored } = await openpgp.revokeKey({
        key: await openpgp.readKey({ armoredKey: privateKeyArmored }),
        format: 'armored' // output armored keys
    });
    console.log(revokedKeyArmored); // '-----BEGIN PGP PUBLIC KEY BLOCK ... '
})();

Sign and verify cleartext messages

(async () => {
    const publicKeyArmored = `-----BEGIN PGP PUBLIC KEY BLOCK-----
...
-----END PGP PUBLIC KEY BLOCK-----`;
    const privateKeyArmored = `-----BEGIN PGP PRIVATE KEY BLOCK-----
...
-----END PGP PRIVATE KEY BLOCK-----`; // encrypted private key
    const passphrase = `yourPassphrase`; // what the private key is encrypted with

    const publicKey = await openpgp.readKey({ armoredKey: publicKeyArmored });

    const privateKey = await openpgp.decryptKey({
        privateKey: await openpgp.readPrivateKey({ armoredKey: privateKeyArmored }),
        passphrase
    });

    const unsignedMessage = await openpgp.createCleartextMessage({ text: 'Hello, World!' });
    const cleartextMessage = await openpgp.sign({
        message: unsignedMessage, // CleartextMessage or Message object
        signingKeys: privateKey
    });
    console.log(cleartextMessage); // '-----BEGIN PGP SIGNED MESSAGE ... END PGP SIGNATURE-----'

    const signedMessage = await openpgp.readCleartextMessage({
        cleartextMessage // parse armored message
    });
    const verificationResult = await openpgp.verify({
        message: signedMessage,
        verificationKeys: publicKey
    });
    const { verified, keyID } = verificationResult.signatures[0];
    try {
        await verified; // throws on invalid signature
        console.log('Signed by key id ' + keyID.toHex());
    } catch (e) {
        throw new Error('Signature could not be verified: ' + e.message);
    }
})();

Create and verify detached signatures

(async () => {
    const publicKeyArmored = `-----BEGIN PGP PUBLIC KEY BLOCK-----
...
-----END PGP PUBLIC KEY BLOCK-----`;
    const privateKeyArmored = `-----BEGIN PGP PRIVATE KEY BLOCK-----
...
-----END PGP PRIVATE KEY BLOCK-----`; // encrypted private key
    const passphrase = `yourPassphrase`; // what the private key is encrypted with

    const publicKey = await openpgp.readKey({ armoredKey: publicKeyArmored });

    const privateKey = await openpgp.decryptKey({
        privateKey: await openpgp.readPrivateKey({ armoredKey: privateKeyArmored }),
        passphrase
    });

    const message = await openpgp.createMessage({ text: 'Hello, World!' });
    const detachedSignature = await openpgp.sign({
        message, // Message object
        signingKeys: privateKey,
        detached: true
    });
    console.log(detachedSignature);

    const signature = await openpgp.readSignature({
        armoredSignature: detachedSignature // parse detached signature
    });
    const verificationResult = await openpgp.verify({
        message, // Message object
        signature,
        verificationKeys: publicKey
    });
    const { verified, keyID } = verificationResult.signatures[0];
    try {
        await verified; // throws on invalid signature
        console.log('Signed by key id ' + keyID.toHex());
    } catch (e) {
        throw new Error('Signature could not be verified: ' + e.message);
    }
})();

Streaming sign and verify Uint8Array data

(async () => {
    var readableStream = new ReadableStream({
        start(controller) {
            controller.enqueue(new Uint8Array([0x01, 0x02, 0x03]));
            controller.close();
        }
    });

    const publicKeyArmored = `-----BEGIN PGP PUBLIC KEY BLOCK-----
...
-----END PGP PUBLIC KEY BLOCK-----`;
    const privateKeyArmored = `-----BEGIN PGP PRIVATE KEY BLOCK-----
...
-----END PGP PRIVATE KEY BLOCK-----`; // encrypted private key
    const passphrase = `yourPassphrase`; // what the private key is encrypted with

    const privateKey = await openpgp.decryptKey({
        privateKey: await openpgp.readPrivateKey({ armoredKey: privateKeyArmored }),
        passphrase
    });

    const message = await openpgp.createMessage({ binary: readableStream }); // or createMessage({ text: ReadableStream<String> })
    const signatureArmored = await openpgp.sign({
        message,
        signingKeys: privateKey
    });
    console.log(signatureArmored); // ReadableStream containing '-----BEGIN PGP MESSAGE ... END PGP MESSAGE-----'

    const verificationResult = await openpgp.verify({
        message: await openpgp.readMessage({ armoredMessage: signatureArmored }), // parse armored signature
        verificationKeys: await openpgp.readKey({ armoredKey: publicKeyArmored })
    });

    for await (const chunk of verificationResult.data) {}
    // Note: you *have* to read `verificationResult.data` in some way or other,
    // even if you don't need it, as that is what triggers the
    // verification of the data.

    try {
        await verificationResult.signatures[0].verified; // throws on invalid signature
        console.log('Signed by key id ' + verificationResult.signatures[0].keyID.toHex());
     } catch (e) {
        throw new Error('Signature could not be verified: ' + e.message);
    }
})();

Documentation

The full documentation is available at openpgpjs.org.

Security Audit

To date the OpenPGP.js code base has undergone two complete security audits from Cure53. The first audit's report has been published here.

Security recommendations

It should be noted that js crypto apps deployed via regular web hosting (a.k.a. host-based security) provide users with less security than installable apps with auditable static versions. Installable apps can be deployed as a Firefox or Chrome packaged app. These apps are basically signed zip files and their runtimes typically enforce a strict Content Security Policy (CSP) to protect users against XSS. This blogpost explains the trust model of the web quite well.

It is also recommended to set a strong passphrase that protects the user's private key on disk.

Development

To create your own build of the library, just run the following command after cloning the git repo. This will download all dependencies, run the tests and create a minified bundle under dist/openpgp.min.js to use in your project:

npm install && npm test

For debugging browser errors, you can run npm start and open http://localhost:8080/test/unittests.html in a browser, or run the following command:

npm run browsertest

How do I get involved?

You want to help, great! It's probably best to send us a message on Gitter before you start your undertaking, to make sure nobody else is working on it, and so we can discuss the best course of action. Other than that, just go ahead and fork our repo, make your changes and send us a pull request! :)

License

GNU Lesser General Public License (3.0 or any later version). Please take a look at the LICENSE file for more information.

openpgpjs's People

Contributors

alexanderwillner avatar alichry avatar arzeth avatar b11z avatar bafs avatar benhc123 avatar chesnokovilya avatar danrr avatar dreamingofelectricsheep avatar evilaliv3 avatar evildvl avatar justinmchase avatar kaylukas avatar larabr avatar leokotschenreuther avatar mahrud avatar misjoinder avatar mmso avatar pbrunschwig avatar robert-nelson avatar rrrooommmaaa avatar sanjanarajan avatar seancolyer avatar sinderw avatar tanx avatar toberndo avatar twiss avatar wiktor-k avatar wussler avatar zartdinov 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  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

openpgpjs's Issues

Error message for compressed messages

Parsing of a compressed message leads to an error message.

Steps to reproduce:

  • open test/encryption.html
  • decrypt zlib compressed message
    -> correct result, message is decrypted

But the following error message is displayed:
ERROR: Error during parsing. This message / key is propably not containing a valid OpenPGP format.

Dearmoring multiple Keys

Hi,

the method read_publicKey(armoredText) should return multiple public keys from armored text, but it doesn't.
After a quick look through the code the openpgp_encoding_deArmor method returns only the first found something dearmored.
But the read_publicKey method expects multiple dearmored keys.

Best regards

function read_publicKey(armoredText) {
  /*....*/
                /*returns only the first found*/
        var input = openpgp_encoding_deArmor(armoredText.replace(/\r/g,'')).openpgp;
        var l = input.length;
        while (mypos != input.length) {
                /*....*/
        }
  /*....*/
}


function openpgp_encoding_deArmor(text) {
    var type = getPGPMessageType(text);
    if (type != 2) {
    var splittedtext = text.split('-----');
    data = { openpgp: openpgp_encoding_base64_decode(splittedtext[2].split('\n\n')[1].split("\n=")[0].replace(/\n- /g,"\n")),
            type: type};
    if (verifyCheckSum(data.openpgp, splittedtext[2].split('\n\n')[1].split("\n=")[1].split('\n')[0]))
        return data;
    else
        util.print_error("Ascii armor integrity check on message failed: '"+splittedtext[2].split('\n\n')[1].split("\n=")[1].split('\n')[0]+"' should be '"+getCheckSum(data))+"'";
    } else {
        var splittedtext = text.split('-----');
        var result = { text: splittedtext[2].replace(/\n- /g,"\n").split("\n\n")[1],
                       openpgp: openpgp_encoding_base64_decode(splittedtext[4].split("\n\n")[1].split("\n=")[0]),
                       type: type};
        if (verifyCheckSum(result.openpgp, splittedtext[4].split("\n\n")[1].split("\n=")[1]))
                return result;
        else
            util.print_error("Ascii armor integrity check on message failed");
    }
}



´´´



modular openpgp.js

openpgp.js is great but also huge.

openppg.js is "modular" because it is split in 43 different .js files, but it would be nice to know which of these files are "must have" and which are optional depending on features someone needs ... i.e.:

  • are all *.js files in packet needed for all actions (168KB)
  • which actions need blowfish.js or twodes.js?
  • if I need RSA only can I delete elgamal (although elgamal.js is on 1KB big)?
  • do I need all files ciphers (272KB in total)
  • when do you need compression (I never needed it)
  • etc

What I tried to do:

  1. I created a js code which created rsa keys, encrypted, signed, decrypted and checked signatures (with these created keys)
  2. started removing one *.js file after another and running scripts/minimize.sh after every remove. I run my js code from step 1 after every remove to see does it still work.
  3. I was able to remove 14 files and have 29 files at the end. openpgp.min.js was 138KB insted of 215KB before deleting.
  4. gzipped is difference even bigger... around 40% smaller

my test code is still working after that but I have no idea when will it stop working because these tests probably do not cover everything I need.

Missing compression (zlib is default)

Carsten wrote:

To be compatible with GnuPG this library is missing compression (zlib is default). I was hoping that this is implemented before we even have key generation ;)

The standard currently defines the following compression algorithms:

ID Algorithm Req.

0 - Uncompressed MUST
1 - ZIP [RFC1951 http://tools.ietf.org/html/rfc1951] SHOULD
2 - ZLIB [RFC1950 http://tools.ietf.org/html/rfc1950] MAY
3 - BZip2 [BZ2 http://tools.ietf.org/html/rfc4880#ref-BZ2] MAY
100 to 110 - Private/Experimental algorithm

Want to back this issue? Post a bounty on it! We accept bounties via Bountysource.

OpenPGP.js breaks at zlib (with fixed Huffman tables)

I received a ZLIB compressed message right now, openpgp.js breaks.

When I tried to track it, it seems like the base64 decoded thing is nonsense (it is a valid base64, but not a valid zip file), and of course what openpgp.js returns is nonsense too.

I can add the messages

the message:

-----BEGIN PGP MESSAGE-----
Charset: ISO-8859-1
Version: GnuPG v1.4.11 (GNU/Linux)
Comment: Using GnuPG with undefined - http://www.enigmail.net/

hIwDuMPn5McVjagBA/46REfOS+d14LGF7Jm1a0EeCj3ZxLgNRZb4BJxE4tMtpM/C
6KcZfdB3+uFhGTA0ZpVu+kR+jNlZ2JpT71wrnhuGPQhYNXZTN8kQJYui2YkeDZTc
eolGkyu4AeT99CagaAAZrww6u4S5cR8mEhFS95NYBoDj2XZKsYNgWm74YnyOM4UB
DAN43uJT5aa0GAEIAIkpsl93MZYadKJO4z2Rmo6Uws6NMjbhCSUVwIB6USr1l4ee
P89N1w8Y+JFOieCl/NcLeuT/d9KpIRm5cg8xo1KlpEYO4fMvpA63LvlF6ohomyIB
5+tROj2E3yy3jflCD4H1odgzmHtbO1lDmTbMUP57d+pJR4ebGTO5Z1l+AYiu1EKI
+2Na/WkMekcOXPc8tvvhbJPrQdluzIV3rHwehsdnX4IwBBFFWydXKnVHsR8mOIvK
2PI9M8zUXomUYHOL/oYTx/yD6r9pRGRQ5SqIoctnLS5Yv5wnpxepaS4DzoWkPvzs
gM7aQH6nVMH7jAyE2GG4vjDQjHRcQ7qtPV4PYUnJ6YKXLTiaIjCMa+todBOTU686
lChURf39DpCsEn24uharkLDmLwWQi6zylLkq74XHPNZQjQ3nGnU78EX/g/988M/Y
z6lN6Fxr8ABU3q5CP74EmNer6epPEyg160WqwB9QbOK/LdB6+MEQTTU4G6NH4/9/
bWc5hRw9xglKPXSY3+UWpqIXOqxNVNu3l8938yGL7UXemtUNKNSNN/uZex8/2nOI
jm5CmzJGCuD17oZC+WZc07TefKDlNYPQwlEY62MG2XJCk8dQsK0r2VomhYzn66hb
+ujrUQtt0N6hI1V4zmq5v8NEBcguNdY6YMMXCzWEyzMbvV3FVgIiH5JcP9EejDuI
+C5KcLWk3dS4hH8ZfmGQwXFllCMBEVoL7v57tuYkfBFvjxNVXYRAl42dnO1zSPEQ
k6cwQGMu47xUUS2OMjpyz4Ypn0XRUtH2kBLIK5SxdQ4ZLa44Ml6whcG+zPNZi6wE
UmVwj+Ya6NdHA4c44pTADcPqzdFImBgHcwOrIIRCzG8JiRhxQsenqjQgQNoakJzE
YNbmRxq639j9d6kO+lmiaKcBo5Whekj8JOwUuei7+BEUAonV8v6bnlRvayBb8Da9
DeCPxIxpUHLJpWP5+EFJMsz6k9kaV2TyABntjho0xux3j17wnAVNJgIJtw860ryP
kSYA1OOqsD95Lq2PQ1TKj50h6Pl0EGtd2E/8P6Gk0qcbs4KDc+V7DCoUzns6FIka
/XHS7haKgoIArL7ep2WqjUzJFezQY9uJmNwG29Q3tsMNC6BU5HfNIybJBYuypEnY
KaM2QBG4wRrg50XTsfxxDv2JzasGYtJFSpz34zwTK7GSE0LeJPaqweh3ZCKwo0Bp
st0ZGv2Z0t9QobUAkTUI
=QSjS
-----END PGP MESSAGE-----

my private key, that I used

-----BEGIN PGP PRIVATE KEY BLOCK-----
Version: OpenPGP.js v.1.20121015
Comment: http://openpgpjs.org

xcE+BFDJmLsBA/9uCsJS8qTLyFOTUZHGdVzmEgTtxsSPG/bAVnHEa8fNWVBd
eqo/vmQgiqHs4r3YywWQzZX8bJmiQWSVF2Wo5RngvKtUiWxElQ6KIVzWvT0+
LTVfPfwlJTZ76j+4Z1spA088Nwg8ytM4scq6Is7t5R4tiDX6SHGJbZGOFOic
hCA9zQARAQAB/gMDAhvHPCZ3h1wVYNPAEFJ+pjr/9LfKeBBkZCkO8moQhB/L
ZOC3fh1jIl+tUp7xpgu1n4TG/fVFLTnBc55u6jQWv6exieRt0j6Exf1UIt/S
VM6/fqAw9lPyYNmP+PaVzooCnVcEc5WPpWfpE+sM47O4NXVd1+vPVZT9NPQB
6Ul0zEKbajZC1QVufIxdcAtla8XO/W2g4UzenkwcARk7Ekhq7L/FtwfGjd3+
pHVX2W20DoUG3ITp8uV4LvLoj6pHrAe9tqMXGXcrkLOwnrUikzOJ1psBuPuf
vY8kC9NYaPQcWM9+TOwk448U8sNsGpQpu2+rAiSOQYcDSATbjqeR5LxnjpHw
lx8LNSryaomWoVgan+6ws91OSQBHRl9ohBGER1M5AbCiJidMSnMRvm6VlsUS
VLWkLyB8H7egLWPL9VvCXkDR1N345skO/orX1ySkQ73xwbkyDKcjzkYgU6gn
cdGv1L+GoraGjUCg11Bqt2mWzR9LYXJlbCBCaWxlayA8a2JAa2FyZWxiaWxl
ay5jb20+wpwEEAECABAFAlDJmLwJELjD5+THFY2oAABGtAP/Yz0yAKUj/kKR
Ws3M2EAmnQffdkKPQqKiYRHmfSRtbKx5KHAbWF5C4YBWmagib24FBKD04ol0
MmyR6IDCgRxZeGq1CLkxgBksRLbmcTQ2hJlOjH0P463tWLl0f2PLOX/FThjK
AUgHr/qPc6XUAybHHe34eFpy3prqlIJdq2H40Fk=
=tQnA
-----END PGP PRIVATE KEY BLOCK-----

GPG decrypts it just fine

by playing with the openPGP.js, I found out it's using (according to OpenPGP.js) fixed huffman tables.... but the code in that part of openPGP is so crazy and full of fliping bits back and forth I can' tell if it's correct or not.

Want to back this issue? Post a bounty on it! We accept bounties via Bountysource.

Basic setup for allow signing of text?

Hello, great work you guys are doing here! Seeing as the documentation is somewhat sparse, i was wondering if it's been covered anywhere what the basic setup would be to allow for just the signing a body of text in a browser? for example, a web-based email client.

Also, can a 2048 bit DSA and Elgamal key be used?

Thanks!
Nick

message decryption works but errors "Error during parsing"

I am able to successfully decrypt this message, but this error still gets printed on the test encryption/decryption page found at test/encryption.html
ERROR: Error during parsing. This message / key is propably not containing a valid OpenPGP format.

Message:

-----BEGIN PGP MESSAGE-----
Version: GnuPG v1.4.11 (GNU/Linux)

hIwDx4bi8LXf2bsBA/wKsmyca+KmivBMfuhtTkztUuwucllgImuZtioAUTHpLKAc
sbP1k5rFAd9DBakhAnD8ONPq4QdjJiEixb0iNW7/tEI2yMFTOLMgm4dojFEUNyiM
cyA4sc2rSp4CKRkIpPs4y0BvhSD8tupJfQW+rFUjH/SGXnQi5bqoMlxW7vqovNJe
Ablm23K/HQpjxsP7fN/iU5U7usYkAHgVMqUt5K46jaqA2w7xeBfJikXBiMNRRunD
5x6l/nyM5urcrD/1+5f/kGXf8URgb/Gfywdn6sjA1kWFOA1L0/UXA4zShBl4NQ==
=zaJH
-----END PGP MESSAGE-----

Private Key (unencrypted):

-----BEGIN PGP PRIVATE KEY BLOCK-----
Version: GnuPG v1.4.11 (GNU/Linux)

lQHYBFAtVlgBBADMBAtPKrZ8z6OfEX7iWDt89IjKzTVxRbrBCVQmId/d1iulX+iy
i/t0rIIn23hlGC33S6jDK86IUEyXNlQBE/EkwW+w5ZBugDKbZCRiP0fTLnjJ0P5a
FUFZqdzOTGQS4szlAU4jT4ceCLFuy7Azga8mMl0fQS84nqsGoOSIQY6u8QARAQAB
AAP+JNOeDv5QzprUmEQaOVVf96BUiPcd7IJ/wZSjgfafaHXejw2O/ZvTGi6BmBw/
0U/aDEe2APUJZQXJrYn7A+7LmloKxuyGGn3w6z4VI0b6P3+LH6dHztqs6VfUXim0
WxWHP6dbxsAg9/k8nWY2AcQC1pyxtaVn+1XTQkYu4Ssg76ECANApFnWHNmx5s12v
rj5cj/7OxqWFYoKPmTDIQEGglTuOrhKvqfF4AhYlCvHo5GGJKwGU/FaCe6v/3fAa
9yYVwZUCAPrnG20zWgHnXP0keetrUSuKALGx4ImnaFbaK6peSR03b/Y0kOnm8XxL
lh+7Rx5RSnIrrC0S8uwIrzdc0ZgSmO0B/Apiw0gRVAZ+OwJS52l3ZZJwK1KfbOsY
qovh+SVtT+RALLs1pKkw+p55r/2Jg5plWsJ7KzwuG9/vqNHKDGkfrf2hurQbbXlr
ZXkxIDxteWtleTFAZXhhbXBsZS5jb20+iLgEEwECACIFAlAtVlgCGwMGCwkIBwMC
BhUIAgkKCwQWAgMBAh4BAheAAAoJEGEk/2vVdiRBnN0D/3IhPwJcV0KQ8ZuQx1Uq
YQvl69Gwpyj901KU2cIp3BKikFyT9Dm6koSc4EyxpzUyljCN2yNKnPRh6m/qcQ9f
Rckr9anoiHuB02y7v/cH8I38h1VRtagq8n9c9eY+e2qWEcIP99fa/h8V4oEPEy9e
SzGg094jpMUhhZg2pf11bh0gnQHYBFAtVlgBBACyqnsuytd4vKRNb8zVW71SYS1+
YHw2gUBwujO62nyep/XC3cXu9yj2Dmfqicc0+7eY3PW9mwCQjZNDuSv+ZEFuoqY1
xqLXcXGXc4t1QO/ZTFkoULp0Dv8bOSbc0TfDmwbotrw0RnsECxrCwclmrjuiXP3Z
xKUsuKBzdgRHHoVNcwARAQABAAP/SMvChgW6tmRyM2T2mgYXhO24WhIY+iI+rc+I
TT1BYml/7oZq49tLkrm66Gp7wqA/Jab972Os3rj4hDL59FxH9gxDJAL5XEjl3q8v
A173hwioyW3FE19horTGDhNsqQUPHba9Xzo9JuioLko0xM/ItSigWfT0c8wTGQVL
8JuUnaECANW2uPYwRWxu21P3mtCLrVSZDTHXcQU3gCEfPasTCccIuQYVLM1gxnaM
Xp7vxBThY3/ufQc10HKhhwUykCiF4BMCANYEedYa7Ce5Yg6Be9zAK3LCd+PNIzLX
XIZAF1t1v0V/j9OFHXXFqpeql5iYAGdXVOV0FDSpSIw0CQpC6l3qSSEB/0PozKxp
9qjTV53vH6l56BmDIzfV9J9qThV+BRpUltEcUIoqIJl41rzsNbM8yKPJ3ucT4gUe
6QBe4j6ryuJBI5SeTIifBBgBAgAJBQJQLVZYAhsMAAoJEGEk/2vVdiRBe0MD/0Rp
AqKLZTE1buir/jKbv6zXzEchMuv4MksIQ0oCDKTUPiyD33BxgSziwSANSkM+RZVL
RQZAq4yeGAlakfwHYSAGJC4BUQAyzAdNPfcEqlsfq/nL6RHKHHD+fb7V8IAV59WU
x+Ncqh8mgzjs+jY7FExVX4tONse4+IylWouo1dNK
=LZNd
-----END PGP PRIVATE KEY BLOCK-----

Result (decrypted message):

Hello, This is a test!

test/encryption.html: undefined var: openpgp_crypto_symmetricDecrypt

I am trying to run the test/encryption.html file straight from the repo. When trying to decrypt a signed+encrypted message I get this error however:

Uncaught exception: ReferenceError: Undefined variable: openpgp_crypto_symmetricDecrypt

Error thrown at line 121, column 2 in decrypt(symmetric_algorithm_type, key) in file://localhost/home/dolf/Projects/OpenPGPjs/src/packet/openpgp.packet.encryptedintegrityprotecteddata.js:
    this.decryptedData = openpgp_crypto_symmetricDecrypt(
called from line 185, column 4 in decrypt(msg, key) in file://localhost/home/dolf/Projects/OpenPGPjs/src/packet/openpgp.packet.encryptedsessionkey.js:
    return msg.encryptedData.decrypt(algo, sesskey);
called from line 48, column 2 in decryptAndVerifySignature(private_key, sessionkey, pubkey) in file://localhost/home/dolf/Projects/OpenPGPjs/src/openpgp.msg.message.js:
    var decrypted = sessionkey.decrypt(this, private_key.keymaterial);
called from line 34, column 8 in decrypt(private_key, sessionkey) in file://localhost/home/dolf/Projects/OpenPGPjs/src/openpgp.msg.message.js:
    return this.decryptAndVerifySignature(private_key, sessionkey).text;
called from line 73, column 3 in run(encrypt) in file://localhost/home/dolf/Projects/OpenPGPjs/test/encryption.html:
    $('#messageoutput').text(msg[0].decrypt(keymat, sesskey));
called from line 1, column 0 in <anonymous function>(event) in file://localhost/home/dolf/Projects/OpenPGPjs/test/encryption.html:
    run(false)

Any thoughts?

  • Platform 64 bit linux
  • browser: opera 11.64 / firefox 12
  • version: latest commit in master

Non-GPGTools repo?

Hey all,

So I'd like it if we started a new github user or repo that wasn't tied to the GPGTools project, as this may give the impression that this is mostly written by Alex, and is a direct subset of the GPGTools project.

I believe that this is going to be something completely different (though obviously GPGTools may use this implementation if you choose to do so), so do we have any better ways of getting just a direct OpenPGPJS repo?I feel like that way it would better solidify the developer base, and be more of an independent project.

Also as for the name, do we want to stick with OpenPGPJS, or change it to something different? JSOpenPGP? OpenJSPGP?

I'll email this to the list as well, as I'd like to hear what everyone has to say about this.

[GPGTools compatibility] decryptSecretMPIs fails

Using openpgpjs / GPG4Browsers with GnuPG/GPGTools generated keys (RSA/RSA) works for encrypting, but not for decrypting messages.

I've generated a test keypair to test with: https://gist.github.com/1501684 (secret: 1234). The following message has been encrypting using the test page and the public key from the gist.

-----BEGIN PGP MESSAGE-----
Version: GPG4Browsers 0.1
Comment: http://openpgpjs.org

wcBMA8Y07v9gO2wgAQf+Lk8u6CZRNhkoq+US/jKNfMt8HGiaI0Wa3u0ZCgoH
TCToj4diP9+OuGZdV2O3WkUvwKG2SsmQXoGnsGxSn5A4uJEl5dJ2QUwaYJKe
lTBxVZSrOenyJhSQbwp9g+WXyQ/unkqeWVqkp86n5697FnsOg7oqYXKJ0qDt
MtjqQtH1ZAj8CoixJXJOaHDEeKLZvNdcMTBUnMR8KStpfcFHavTxSyUe/Xzk
LKY8aFVI/zVnPvitxJ0GCXKGBXWnAcIYJZtiIPqB/Us736CbJFm75D69/kg7
0f2DYjzPPx/CmpkEnfEVM+GpE0UtVlxBOCACY6/loDPY4NWM75HPw1KLVUwA
E9JPAQCZE1tXfKwRZx+blTrnxSMy58yo6csuBZEGAXYURar+yqU59y3P0VDd
FNcrAUSFfG8oYjtlfNTj9KMI8vzlEvAy+roTyk8XqWpg2ARs9w==
=wvBO
-----END PGP MESSAGE-----

Decrypting the message using gnupg and the above credentials work fine.
Decrypting the message on the test page fails with an ERROR: Password for secrect key was incorrect!

I traced it back to within openpgp.packet.keymaterial decryptSecretMPIs , and especially the if-case in l. 416ff.
s2usageConvention is 254. I am looking into this and added this here for reference. Is there a compatibility issue with GPGTools itself or can someone verify this using a different GnuPG (i.e., not the Mac GPGTools)?

var cleartextMPIslength = cleartextMPIs.length;
if (this.s2kUsageConventions == 254 && str_sha1(cleartextMPIs.substring(0,cleartextMPIs.length - 20)) == cleartextMPIs.substring(cleartextMPIs.length - 20)) {
   cleartextMPIslength -= 20;
} ...

The corresponding passage in the RFC 4880 is on page 42:

If the string-to-key usage octet was 254, then a 20-octet SHA-1 hash of the plaintext of the algorithm-specific portion.

On a related note: At line 421, there is a typo "s2kUsageConcentions" should be "s2kUsageConventions". It's nothing much really, too silly for a diff or pull request.

Memory Leak

There seems to be a memory leak of some sort, when encrypting/decrypting. E.g. when processing 3 - 4 MB of data, the browser tab memory usage quickly goes above 100 MB in Chrome 17.0.963.56

Add make deploy

  • make deploy -> invoke below mentioned targets
  • make deploy-jslib -> OpenPGP.js.zip with README, example page, etc. and upload binary to GitHub
  • make deploy-chrome -> Chrome crx plugin and upload binary to GitHub
  • ...

UTF-8 Support

Support more than ascii characters.. RFC 4880 indicates that the default encoding is in fact UTF-8, we should be more mindful of interpreting characters appropriately that don't fit in the backwards compatible ascii/utf-8 range.

Local key storage and lookup.

For integration purposes, I was thinking about eventually writing some helper functions to keep public keys stored in an intelligent manner. For maximum use cases, I was thinking of using HTML5's sessionStorage object, and if that's not available expose the same calls, but store them as cookies.

As for look-up purposes, I was thinking about writing a number of different examples in different languages that people can use to actually do public key lookups in a meaningful way (PGP, JSP, CGI, etc) to compliment the storage of them (getting around that pesky SOP :-P).

Calculate/show (in %) how far is key generation process?

This is not really an issue but more support question because I did not see any information about mailing list for users.

2048 key generation takes between 1 and 30 seconds, obviously depending on many factors. Is there a way to/to show in % how far is this process to be able to have a progress bar or something similar where user could be informed how long does he have to wait?

I was looking around BigInteger functions but I did not come very far.

Thanks and sorry for spam if there is a better place for this question.

Want to back this issue? Post a bounty on it! We accept bounties via Bountysource.

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.