Skip to content

vaultys/saltpack

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

55 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@vaultys/saltpack

A Javascript implementation of Keybase's Saltpack encrypted/signed messaging format.

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

@vaultys/saltpack works on node and browser (you need to use crypto-browserify, vm-browserify and stream-browserify)

Installation

@vaultys/saltpack is published to the npm registry. TypeScript definitions are included.

npm install @vaultys/saltpack

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 '@vaultys/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 ...

@vaultys/saltpack also supports streaming encryption with EncryptAndArmorStream or (EncryptStream for encrypting without armor).

import {EncryptAndArmorStream} from '@vaultys/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 '@vaultys/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 '@vaultys/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 '@vaultys/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 '@vaultys/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 '@vaultys/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 '@vaultys/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 '@vaultys/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 '@vaultys/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 '@vaultys/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 '@vaultys/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 '@vaultys/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 '@vaultys/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 '@vaultys/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 '@vaultys/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...

Additional notes

  • @vaultys/saltpack always chunks input data to 1 MB payloads.
  • @vaultys/saltpack is partially tested with Keybase:
    • Encrypted messages created by @vaultys/saltpack can be decrypted with Keybase.
    • Signcrypted messages created by @vaultys/saltpack can be decrypted with Keybase.
    • Signed messages created by Keybase can be verified with @vaultys/saltpack.
    • Signed messages created by @vaultys/saltpack can be read by Keybase.

License

@vaultys/saltpack is a fork of @samuelthomas2774/saltpack. @vaultys/saltpack is released under the MIT license. Saltpack is designed by the Keybase developers, and uses NaCl for crypto and MessagePack for binary encoding. @vaultys/saltpack uses TweetNaCl.js. @vaultys/saltpack armoring implementation is based on saltpack-ruby.

About

TypeScript implementation of [Saltpack](https://saltpack.org)

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • TypeScript 75.5%
  • JavaScript 24.5%