npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@samuelthomas2774/saltpack

v0.4.0

Published

Node.js implementation of Saltpack.

Downloads

11

Readme

node-saltpack

A Node.js/TypeScript implementation of Keybase's Saltpack encrypted/signed messaging format.

node-saltpack implements version 2.0 of Saltpack. All message types (encryption, attached signing, detached signing and signcryption) are supported.

Installation

node-saltpack is published to the npm registry and GitHub Package Registry. TypeScript definitions are included.

npm install @samuelthomas2774/saltpack

GitHub Package Registry

By default npm will install from https://npmjs.com. You can configure npm to install node-saltpack from GitHub Package Registry by adding this to your npmrc:

@samuelthomas2774:registry=https://npm.pkg.github.com
echo "@samuelthomas2774:registry=https://npm.pkg.github.com" >> `npm --global prefix`/etc/npmrc

Encryption

encryptAndArmor encrypts a string or Uint8Array (or a Node.js Buffer) and returns the ASCII-armored encrypted data as a string.

encrypt accepts the same arguments as encryptAndArmor but returns a Buffer without armor.

import {encryptAndArmor} from '@samuelthomas2774/saltpack';
import * as tweetnacl from 'tweetnacl';

const plaintext: Buffer | string = '...';
const sender_keypair: tweetnacl.BoxKeyPair = tweetnacl.box.keyPair();
const recipients_keys: Uint8Array[] = [
    tweetnacl.box.keyPair().publicKey,
];

const encrypted = await encryptAndArmor(plaintext, sender_keypair, recipients_keys);

// encrypted === 'BEGIN SALTPACK ENCRYPTED MESSAGE. keDIDMQWYvVR58B FTfTeD305h3lDop TELGyPzBAAawRfZ rss3XwjQHK0irv7 rNIcmnvmn5YlTtK 7O1fFPePZGpx46P ...

node-saltpack also supports streaming encryption with EncryptAndArmorStream or (EncryptStream for encrypting without armor).

import {EncryptAndArmorStream} from '@samuelthomas2774/saltpack';
import * as tweetnacl from 'tweetnacl';

const sender_keypair: tweetnacl.BoxKeyPair = tweetnacl.box.keyPair();
const recipients_keys: Uint8Array[] = [
    tweetnacl.box.keyPair().publicKey,
];

const stream = new EncryptAndArmorStream(sender_keypair, recipients_keys);

stream.end('...');

// Write the encrypted and armored data to stdout
stream.pipe(process.stdout);

Messages can be decrypted with dearmorAndDecrypt (or decrypt if the message isn't armored).

import {dearmorAndDecrypt} from '@samuelthomas2774/saltpack';
import * as tweetnacl from 'tweetnacl';

const encrypted: string = 'BEGIN SALTPACK ENCRYPTED MESSAGE. keDIDMQWYvVR58B FTfTeD305h3lDop TELGyPzBAAawRfZ rss3XwjQHK0irv7 rNIcmnvmn5YlTtK 7O1fFPePZGpx46P ...';
const recipient_keypair: tweetnacl.BoxKeyPair = tweetnacl.box.keyPair();

// If you know the sender's public key you can pass it to dearmorAndDecrypt and it will throw if it doesn't match
const sender_key: Uint8Array = tweetnacl.box.keyPair().publicKey;

try {
    const decrypted = await dearmorAndDecrypt(encrypted, recipient_keypair, sender_key);

    // If you didn't pass the sender's public key you should check it now
    if (!Buffer.from(decrypted.sender_public_key).equals(sender_keys)) {
        throw new Error('Sender public key doesn\'t match');
    }

    // decrypted === '...'
} catch (err) {
    console.error(err);
}

Decryption also supports streaming with DearmorAndDecryptStream or DecryptStream.

import {DearmorAndDecryptStream} from '@samuelthomas2774/saltpack';
import * as tweetnacl from 'tweetnacl';

const recipient_keypair: tweetnacl.BoxKeyPair = tweetnacl.box.keyPair();

// If you know the sender's public key you can pass it to DearmorAndDecryptStream and it will emit an error if it doesn't match
const sender_key: Uint8Array = tweetnacl.box.keyPair().publicKey;

const stream = new DearmorAndDecryptStream(recipient_keypair, sender_key);

stream.on('end', () => {
    // If you didn't pass the sender's public key you should check it now
    if (!Buffer.from(stream.sender_public_key).equals(sender_keys)) {
        throw new Error('Sender public key doesn\'t match');
    }
});
stream.on('error', err => {
    console.error(err);
});

stream.end('BEGIN SALTPACK ENCRYPTED MESSAGE. keDIDMQWYvVR58B FTfTeD305h3lDop TELGyPzBAAawRfZ rss3XwjQHK0irv7 rNIcmnvmn5YlTtK 7O1fFPePZGpx46P ...');

// Write the decrypted data to stdout
stream.pipe(process.stdout);

Signing

signAndArmor signs a string or Uint8Array (or a Node.js Buffer) and returns the ASCII-armored signed data as a string.

sign accepts the same arguments as signAndArmor but returns a Buffer without armor.

import {signAndArmor} from '@samuelthomas2774/saltpack';
import * as tweetnacl from 'tweetnacl';

const plaintext: Buffer | string = '...';
const signing_keypair: tweetnacl.SignKeyPair = tweetnacl.sign.keyPair();

const signed = await signAndArmor(plaintext, signing_keypair);

// signed === 'BEGIN SALTPACK SIGNED MESSAGE. kYM5h1pg6qz9UMn j6G9T0lmMjkYOsZ Kn4Acw58u39dn3B kmdpuvqpO3t2QdM CnBX5wO1ZIO8LTd knNlCR0WSEC0000 ...

Streaming is supported with SignAndArmorStream or SignStream.

import {SignAndArmorStream} from '@samuelthomas2774/saltpack';
import * as tweetnacl from 'tweetnacl';

const signing_keypair: tweetnacl.SignKeyPair = tweetnacl.sign.keyPair();

const stream = new SignAndArmorStream(signing_keypair);

stream.end('...');

// Write the signed and armored data to stdout
stream.pipe(process.stdout);

Signed messages can be verified and read with dearmorAndVerify or verify.

import {dearmorAndVerify} from '@samuelthomas2774/saltpack';
import * as tweetnacl from 'tweetnacl';

const signed: string = 'BEGIN SALTPACK SIGNED MESSAGE. kYM5h1pg6qz9UMn j6G9T0lmMjkYOsZ Kn4Acw58u39dn3B kmdpuvqpO3t2QdM CnBX5wO1ZIO8LTd knNlCR0WSEC0000 ...';

// If you know the sender's public key you can pass it to dearmorAndVerify and it will throw if it doesn't match
const sender_key: Uint8Array = tweetnacl.sign.keyPair().publicKey;

try {
    const verified = await dearmorAndVerify(signed, sender_key);

    // If you didn't pass the sender's public key you should check it now
    if (!Buffer.from(verified.public_key).equals(sender_key)) {
        throw new Error('Sender public key doesn\'t match');
    }

    // verified === '...'
} catch (err) {
    console.error(err);
}

Reading signed messages also supports streaming with DearmorAndVerifyStream or VerifyStream.

import {DearmorAndVerifyStream} from '@samuelthomas2774/saltpack';
import * as tweetnacl from 'tweetnacl';

// If you know the sender's public key you can pass it to DearmorAndVerifyStream and it will emit an error if it doesn't match
const sender_key: Uint8Array = tweetnacl.sign.keyPair().publicKey;

const stream = new DearmorAndVerifyStream(sender_key);

stream.on('end', () => {
    // If you didn't pass the sender's public key you should check it now
    if (!Buffer.from(stream.public_key).equals(sender_keys)) {
        throw new Error('Sender public key doesn\'t match');
    }
});
stream.on('error', err => {
    console.error(err);
});

stream.end('BEGIN SALTPACK SIGNED MESSAGE. kYM5h1pg6qz9UMn j6G9T0lmMjkYOsZ Kn4Acw58u39dn3B kmdpuvqpO3t2QdM CnBX5wO1ZIO8LTd knNlCR0WSEC0000 ...');

// Write the decrypted data to stdout
stream.pipe(process.stdout);

Detached signing

signDetachedAndArmor signs a string or Uint8Array (or a Node.js Buffer) and returns the ASCII-armored signature as a string.

signDetached accepts the same arguments as signDetachedAndArmor but returns a Buffer without armor.

Detached signing/verifying does not support streaming yet.

import {signDetachedAndArmor} from '@samuelthomas2774/saltpack';
import * as tweetnacl from 'tweetnacl';

const plaintext: Buffer | string = '...';
const signing_keypair: tweetnacl.SignKeyPair = tweetnacl.sign.keyPair();

const signed = await signDetachedAndArmor(plaintext, signing_keypair);

// signed === 'BEGIN SALTPACK DETACHED SIGNATURE. kYM5h1pg6qz9UMn j6G9T0tZQlxoky3 0YoKQ4s21IrFv3B kmdpuvqpO3t2QdM CnBX5wO1ZIO8LTd knNlCR0WSEC0000 ...

Detached signatures can be verified with dearmorAndVerifyDetached or verifyDetached.

import {dearmorAndVerifyDetached} from '@samuelthomas2774/saltpack';
import * as tweetnacl from 'tweetnacl';

const signed: string = 'BEGIN SALTPACK SIGNED MESSAGE. kYM5h1pg6qz9UMn j6G9T0lmMjkYOsZ Kn4Acw58u39dn3B kmdpuvqpO3t2QdM CnBX5wO1ZIO8LTd knNlCR0WSEC0000 ...';
const plaintext: Buffer | string = '...';

// If you know the sender's public key you can pass it to dearmorAndVerifyDetached and it will throw if it doesn't match
const sender_key: Uint8Array = tweetnacl.sign.keyPair().publicKey;

try {
    const result = await dearmorAndVerifyDetached(signature, plaintext, sender_key);

    // If you didn't pass the sender's public key you should check it now
    if (!Buffer.from(result.public_key).equals(sender_key)) {
        throw new Error('Sender public key doesn\'t match');
    }
} catch (err) {
    console.error(err);
}

Signcryption

Signcryption is very similar to Saltpack's usual encryption format, but:

  • The sender uses an Ed25519 signing key instead of an X25519 encryption key,
  • A symmetric key can be provided for a group of recipients instead of each recipient having their own encryption key, and
  • Messages are not repudiable, which means anyone who has a copy of the message and a decryption key can verify it's authenticity, not just intended recipients.

signcryptAndArmor encrypts a string or Uint8Array (or a Node.js Buffer) and returns the ASCII-armored signcrypted data as a string.

signcrypt accepts the same arguments as signcryptAndArmor but returns a Buffer without armor.

import {signcryptAndArmor, SymmetricKeyRecipient} from '@samuelthomas2774/saltpack';
import * as tweetnacl from 'tweetnacl';

const plaintext: Buffer | string = '...';
const sender_keypair: tweetnacl.SignKeyPair = tweetnacl.sign.keyPair();
const recipients_keys: (Uint8Array | SymmetricKeyRecipient)[] = [
    tweetnacl.box.keyPair().publicKey,
];

const signcrypted = await signcryptAndArmor(plaintext, sender_keypair, recipients_keys);

// signcrypted === 'BEGIN SALTPACK ENCRYPTED MESSAGE. keDIDMQWYvVR58B FTfTeDQNHnhYI5G UXZkLqLqVvhmpfZ rss3XwjQHK0irv7 rNIcmnvmn5RTzTR OPZLLRr1s0DEZtS ...

Streaming is supported with SigncryptAndArmorStream or (SigncryptStream for encrypting without armor).

import {SigncryptAndArmorStream, SymmetricKeyRecipient} from '@samuelthomas2774/saltpack';
import * as tweetnacl from 'tweetnacl';

const sender_keypair: tweetnacl.SignKeyPair = tweetnacl.sign.keyPair();
const recipients_keys: (Uint8Array | SymmetricKeyRecipient)[] = [
    tweetnacl.box.keyPair().publicKey,
];

const stream = new SigncryptAndArmorStream(sender_keypair, recipients_keys);

stream.end('...');

// Write the signcrypted and armored data to stdout
stream.pipe(process.stdout);

Symmetric recipient keys can be used by passing a SymmetricKeyRecipient instance. You must provide a unique 32-byte recipient identifier for each symmetric key recipient.

import {signcryptAndArmor, SymmetricKeyRecipient} from '@samuelthomas2774/saltpack';
import * as tweetnacl from 'tweetnacl';

const recipients_keys: (Uint8Array | SymmetricKeyRecipient)[] = [
    new SymmetricKeyRecipient(
        Buffer.from('0000000000000000000000000000000000000000000000000000000000000000', 'hex'), // recipient identifier
        Buffer.from('0000000000000000000000000000000000000000000000000000000000000000', 'hex'), // shared symmetric key
    ),
];

// Use signcrypt, signcryptAndArmor, SigncryptStream or SigncryptAndArmorStream...

Messages can be decrypted with dearmorAndDesigncrypt (or designcrypt if the message isn't armored).

import {dearmorAndDesigncrypt, SymmetricKeyRecipient} from '@samuelthomas2774/saltpack';
import * as tweetnacl from 'tweetnacl';

const encrypted: string = 'BEGIN SALTPACK ENCRYPTED MESSAGE. keDIDMQWYvVR58B FTfTeDQNHnhYI5G UXZkLqLqVvhmpfZ rss3XwjQHK0irv7 rNIcmnvmn5RTzTR OPZLLRr1s0DEZtS ...';
// TODO: how can multiple keys be provided (as a recipient may have multiple shared symmetric keys that may be used for this message)
const recipient_keys: tweetnacl.BoxKeyPair | SymmetricKeyRecipient = tweetnacl.box.keyPair();

// If you know the sender's public key you can pass it to dearmorAndDesigncrypt and it will throw if it doesn't match
const sender_key: Uint8Array = tweetnacl.sign.keyPair().publicKey;

try {
    const decrypted = await dearmorAndDesigncrypt(encrypted, recipient_keys, sender_key);

    // If you didn't pass the sender's public key you should check it now
    if (!Buffer.from(decrypted.sender_public_key).equals(sender_keys)) {
        throw new Error('Sender public key doesn\'t match');
    }

    // decrypted === '...'
} catch (err) {
    console.error(err);
}

Decryption also supports streaming with DearmorAndDesigncryptStream or DesigncryptStream.

import {DearmorAndDesigncryptStream, SymmetricKeyRecipient} from '@samuelthomas2774/saltpack';
import * as tweetnacl from 'tweetnacl';

// TODO: how can multiple keys be provided (as a recipient may have multiple shared symmetric keys that may be used for this message)
const recipient_keys: tweetnacl.BoxKeyPair | SymmetricKeyRecipient = tweetnacl.box.keyPair();

// If you know the sender's public key you can pass it to DearmorAndDesigncryptStream and it will emit an error if it doesn't match
const sender_key: Uint8Array = tweetnacl.sign.keyPair().publicKey;

const stream = new DearmorAndDesigncryptStream(recipient_keys, sender_key);

stream.on('end', () => {
    // If you didn't pass the sender's public key you should check it now
    if (!Buffer.from(stream.sender_public_key).equals(sender_keys)) {
        throw new Error('Sender public key doesn\'t match');
    }
});
stream.on('error', err => {
    console.error(err);
});

stream.end('BEGIN SALTPACK ENCRYPTED MESSAGE. keDIDMQWYvVR58B FTfTeDQNHnhYI5G UXZkLqLqVvhmpfZ rss3XwjQHK0irv7 rNIcmnvmn5RTzTR OPZLLRr1s0DEZtS ...');

// Write the decrypted data to stdout
stream.pipe(process.stdout);

Symmetric keys can be used by passing a SymmetricKeyRecipient instance. You must provide the recipient's unique 32-byte recipient identifier.

import {dearmorAndDesigncrypt, SymmetricKeyRecipient} from '@samuelthomas2774/saltpack';
import * as tweetnacl from 'tweetnacl';

// TODO: how can multiple keys be provided (as a recipient may have multiple shared symmetric keys that may be used for this message)
const recipient_keys: tweetnacl.BoxKeyPair | SymmetricKeyRecipient = new SymmetricKeyRecipient(
    Buffer.from('0000000000000000000000000000000000000000000000000000000000000000', 'hex'), // recipient identifier
    Buffer.from('0000000000000000000000000000000000000000000000000000000000000000', 'hex'), // shared symmetric key
);

// Use designcrypt, dearmorAndDesigncrypt, DesigncryptStream or DearmorAndDesigncryptStream...

Keybase paper keys

Keybase paper keys can be used with KeybasePaperKey.from.

import {dearmorAndDecrypt, KeybasePaperKey, signAndArmor} from '@samuelthomas2774/saltpack';

const paper_key: string = '...';

const keys = await KeybasePaperKey.from(paper_key);

// Encryption
const encrypted: string = 'BEGIN SALTPACK ENCRYPTED MESSAGE. keDIDMQWYvVR58B FTfTeD305h3lDop TELGyPzBAAawRfZ rss3XwjQHK0irv7 rNIcmnvmn5YlTtK 7O1fFPePZGpx46P ...';
const recipient_keypair: tweetnacl.BoxKeyPair = keys.encryption_keypair;

const decrypted = await dearmorAndDecrypt(encrypted, recipient_keypair);

// decrypted === '...'

// Signing
const plaintext: Buffer | string = '...';
const signing_keypair: tweetnacl.SignKeyPair = keys.signing_keypair;

const signed = await signAndArmor(plaintext, signing_keypair);

// signed === 'BEGIN SALTPACK SIGNED MESSAGE. kYM5h1pg6qz9UMn j6G9T0lmMjkYOsZ Kn4Acw58u39dn3B kmdpuvqpO3t2QdM CnBX5wO1ZIO8LTd knNlCR0WSEC0000 ...

Additional notes

  • node-saltpack always chunks input data to 1 MB payloads.
  • node-saltpack is fully tested with php-saltpack.
  • node-saltpack is partially tested with Keybase:
    • Encrypted messages created by node-saltpack and php-saltpack can be decrypted with Keybase.
    • Signcrypted messages created by node-saltpack and php-saltpack can be decrypted with Keybase.
    • Signed messages created by Keybase can be verified with node-saltpack and php-saltpack.
    • Signed messages created by node-saltpack and php-saltpack can be read by Keybase.

License

node-saltpack is released under the MIT license. Saltpack is designed by the Keybase developers, and uses NaCl for crypto and MessagePack for binary encoding. node-saltpack uses TweetNaCl.js. node-saltpack and php-saltpack's armoring implementation is based on saltpack-ruby.