Skip to main content

skiff-crypto-v2

skiff-crypto-v2 is the next generation of Skiff’s cryptographic library, providing enhanced encryption capabilities with an improved datagram system that separates headers and bodies, along with comprehensive PGP support for email encryption.
Version 2.0.0 introduces breaking changes from v1, particularly in the datagram API structure.

Features

  • Enhanced Datagram System: Separate header/body encryption with protobuf support
  • PGP/OpenPGP Integration: Full support for PGP key management and email encryption
  • Improved AEAD: ChaCha20Poly1305 with enhanced metadata handling
  • WKD Support: Web Key Directory integration for automatic key discovery
  • Backward Compatibility: Re-exports core functions from skiff-crypto v1

Installation

npm install skiff-crypto-v2

Key Differences from v1

Datagram API Changes

The primary difference is the datagram system now supports separate header and body encryption: v1 (skiff-crypto):
// Single content parameter
encryptSymmetric<T>(content: T, symmetricKey: string, datagram: Datagram<T>)
v2 (skiff-crypto-v2):
// Separate header and body parameters
encryptSymmetric<Header, Body>(
  header: Header,
  body: Body,
  symmetricKey: string,
  datagram: Datagram<Header, Body>
)

Enhanced Metadata

V2 returns additional metadata with decryption results:
// V2 returns metadata along with decrypted content
{
  header: Header;
  body: Body;
  metadata: AADMeta; // Additional authenticated data metadata
}

Core Encryption Functions

encryptSymmetric

Symmetric encryption with separate header and body support.
function encryptSymmetric<Header, Body>(
  header: Header,
  body: Body,
  symmetricKey: string,
  datagram: Datagram<Header, Body>
): string
header
Header
required
The object header being serialized (typically metadata)
body
Body
required
The object body being serialized (typically the main content)
symmetricKey
string
required
Base64-encoded symmetric key
datagram
Datagram<Header, Body>
required
Mechanism to serialize/deserialize header and body
Returns: Base64-encoded encrypted payload Example:
import { encryptSymmetric, createProtoWrapperDatagramV2 } from 'skiff-crypto-v2';

const datagram = createProtoWrapperDatagramV2(
  'ddl://document',
  HeaderProto,
  BodyProto
);

const encrypted = encryptSymmetric(
  { version: '1.0', type: 'document' },
  { content: 'Hello, World!' },
  symmetricKey,
  datagram
);
Source: libs/skiff-crypto-v2/src/utils.ts:41

decryptSymmetric

Symmetric decryption with metadata extraction.
function decryptSymmetric<Header, Body>(
  message: string,
  symmetricKey: string,
  DatagramType: Datagram<Header, Body>
): { header: Header; body: Body; metadata: AADMeta }
message
string
required
Base64-encoded encrypted payload
symmetricKey
string
required
Base64-encoded key used for decryption
DatagramType
Datagram<Header, Body>
required
The datagram type for deserialization
Returns:
header
Header
required
Decrypted header object
body
Body
required
Decrypted body object
metadata
AADMeta
required
Additional authenticated data metadata
Example:
const { header, body, metadata } = decryptSymmetric(
  encrypted,
  symmetricKey,
  datagram
);

console.log('Header:', header);
console.log('Body:', body);
console.log('Metadata:', metadata);
Source: libs/skiff-crypto-v2/src/utils.ts:82

rawEncryptSymmetric

Symmetric encryption returning raw bytes without base64 encoding.
function rawEncryptSymmetric<Header, Body>(
  header: Header,
  body: Body,
  symmetricKey: string,
  datagram: Datagram<Header, Body>
): Uint8Array
header
Header
required
The object header being serialized
body
Body
required
The object body being serialized
symmetricKey
string
required
Base64-encoded symmetric key
datagram
Datagram<Header, Body>
required
Mechanism to serialize header and body
Returns: Uint8Array encrypted payload Source: libs/skiff-crypto-v2/src/utils.ts:18

rawDecryptSymmetric

Symmetric decryption from raw bytes.
function rawDecryptSymmetric<Header, Body>(
  message: Uint8Array,
  symmetricKey: string,
  DatagramType: Datagram<Header, Body>
): { header: Header; body: Body; metadata: AADMeta }
message
Uint8Array
required
Encrypted payload as raw bytes
symmetricKey
string
required
Base64-encoded key used for decryption
DatagramType
Datagram<Header, Body>
required
The datagram type for deserialization
Returns: Object with decrypted header, body, and metadata Source: libs/skiff-crypto-v2/src/utils.ts:60

Session Key Management

encryptSessionKey

Encrypt a document session key for sharing.
function encryptSessionKey(
  sessionKey: SessionKey,
  myPrivateKey: string,
  myPublicKey: PublicKey,
  theirPublicKey: PublicKey
): EncryptedKey
sessionKey
string
required
Document session key to encrypt
myPrivateKey
string
required
Encryption private key used to encrypt session key
myPublicKey
PublicKey
required
Current user’s encryption public key
theirPublicKey
PublicKey
required
Other user’s encryption public key
Returns:
encryptedKey
string
required
The encrypted session key
encryptedBy
PublicKey
required
Public key of the user who encrypted it
Source: libs/skiff-crypto-v2/src/core.ts:22

decryptSessionKey

Decrypt a document session key.
function decryptSessionKey(
  encryptedSessionKey: string,
  myPrivateKey: string,
  theirPublicKey: Pick<PublicKey, 'key'>
): SessionKey
encryptedSessionKey
string
required
Encrypted copy of the document session key
myPrivateKey
string
required
Current user’s encryption private key
theirPublicKey
object
required
User who encrypted key’s encryption public key
Returns: Decrypted session key string Source: libs/skiff-crypto-v2/src/core.ts:43

signEncryptedSessionKey

Sign an encrypted document session key.
function signEncryptedSessionKey(
  encryptedKey: string,
  mySigningPrivateKey: string
): string
encryptedKey
string
required
Encrypted key field to sign
mySigningPrivateKey
string
required
Signing private key used to generate signature
Returns: Base64-encoded signature Source: libs/skiff-crypto-v2/src/core.ts:60

Datagram V2 Functions

encryptDatagramV2

Encrypt a datagram with header and body.
function encryptDatagramV2<Header, Body>(
  datagram: Datagram<Header, Body>,
  header: Header,
  body: Body,
  sessionKey: string
): EncryptedDataOutput
datagram
Datagram<Header, Body>
required
Datagram definition for serialization
header
Header
required
Datagram header (typically metadata)
body
Body
required
Datagram body (main content)
sessionKey
string
required
Symmetric document encryption key
Returns:
encryptedData
string
required
Base64-encoded encrypted datagram
Source: libs/skiff-crypto-v2/src/core.ts:74

decryptDatagramV2

Decrypt a datagram.
function decryptDatagramV2<Header, Body>(
  datagram: Datagram<Header, Body>,
  sessionKey: string,
  encryptedDatagram: string
): { header: Header; body: Body; metadata: AADMeta }
datagram
Datagram<Header, Body>
required
Datagram definition for deserialization
sessionKey
string
required
Symmetric document encryption key
encryptedDatagram
string
required
Encrypted datagram data
Returns: Object with decrypted header, body, and metadata Source: libs/skiff-crypto-v2/src/core.ts:93

createProtoWrapperDatagramV2

Create a datagram that uses Protocol Buffers for serialization.
function createProtoWrapperDatagramV2<Header, Body>(
  type: string,
  headerClass: ProtoClass<Header>,
  bodyClass: ProtoClass<Body>,
  version?: string,
  versionConstraint?: Range
): Datagram<Header, Body>
type
string
required
Datagram type identifier
headerClass
ProtoClass<Header>
required
Protobuf class for header serialization
bodyClass
ProtoClass<Body>
required
Protobuf class for body serialization
version
string
default:"0.1.0"
Datagram version
versionConstraint
Range
default:"0.1.*"
Datagram version constraint
Returns: Datagram instance with protobuf serialization Example:
import { createProtoWrapperDatagramV2 } from 'skiff-crypto-v2';
import { DocumentHeader, DocumentBody } from './generated/protos';

const DocumentDatagram = createProtoWrapperDatagramV2(
  'ddl://skiff/document',
  DocumentHeader,
  DocumentBody,
  '1.0.0'
);
Source: libs/skiff-crypto-v2/src/datagramClasses.ts:7

PGP Support

generatePGPKey

Generate a PGP key pair.
async function generatePGPKey(
  userIDs: UserID[],
  type?: GenerateKeyOptions['type'],
  curve?: GenerateKeyOptions['curve']
): Promise<PGPKeyPair>
userIDs
UserID[]
required
Array of user IDs for the key (name and email)
type
string
default:"ecc"
Key type (ecc, rsa, etc.)
curve
string
default:"curve25519"
ECC curve name
Returns:
privateKey
Uint8Array
required
Binary format private key
publicKey
Uint8Array
required
Binary format public key
date
Date
required
Key creation date
userIDs
UserID[]
required
User IDs associated with the key
Example:
import { generatePGPKey } from 'skiff-crypto-v2';

const { privateKey, publicKey } = await generatePGPKey([
  { name: 'John Doe', email: '[email protected]' }
]);
Source: libs/skiff-crypto-v2/src/pgp/keyManagement.ts:28

exportPGPKey

Export a PGP key in armored format.
async function exportPGPKey(
  userIDs: UserID[],
  currentPrivateKey: PrivateKey,
  createdAt: Date,
  passphrase?: string
): Promise<{ privateKey: string; publicKey: string }>
userIDs
UserID[]
required
User IDs for the key
currentPrivateKey
PrivateKey
required
Private key to export
createdAt
Date
required
Key creation date
passphrase
string
Optional passphrase to encrypt the exported key
Returns: Object with armored private and public keys Source: libs/skiff-crypto-v2/src/pgp/keyManagement.ts:55

readPrivateKey

Read a PGP private key from binary format.
async function readPrivateKey(
  privateKeyData: Uint8Array
): Promise<PrivateKey>
privateKeyData
Uint8Array
required
Binary private key data
Returns: PrivateKey object Source: libs/skiff-crypto-v2/src/pgp/keyManagement.ts:82

readArmoredPrivateKey

Read a PGP private key from armored format.
async function readArmoredPrivateKey(
  privateKeyData: string,
  passphrase?: string | null
): Promise<PrivateKey>
privateKeyData
string
required
Armored private key string
passphrase
string
Passphrase to decrypt the key if encrypted
Returns: PrivateKey object Source: libs/skiff-crypto-v2/src/pgp/keyManagement.ts:92

readPublicKey / readArmoredPublicKey

Read PGP public keys from binary or armored format.
async function readPublicKey(key: Uint8Array): Promise<PublicKey>
async function readArmoredPublicKey(key: string): Promise<PublicKey>
Source: libs/skiff-crypto-v2/src/pgp/keyManagement.ts:105, 117

PGP Message Encryption

encryptMessage

Encrypt a message for multiple recipients using PGP.
async function encryptMessage(
  binary: Uint8Array | ReadableStream,
  sender: SenderPGPInfo,
  recipients: RecipientPGPInfo[]
): Promise<ReadableStream<string>>
binary
Uint8Array | ReadableStream
required
Message content to encrypt
sender
SenderPGPInfo
required
Sender information with private key for signing
recipients
RecipientPGPInfo[]
required
Array of recipients with public keys
Returns: ReadableStream of armored encrypted message Example:
import { encryptMessage } from 'skiff-crypto-v2';

const encrypted = await encryptMessage(
  new TextEncoder().encode('Hello, World!'),
  {
    name: 'Alice',
    email: '[email protected]',
    privateKey: alicePrivateKey,
    keyID: aliceKeyID
  },
  [
    {
      name: 'Bob',
      email: '[email protected]',
      publicKey: bobPublicKey,
      keyID: bobKeyID
    }
  ]
);
Source: libs/skiff-crypto-v2/src/pgp/encrypt.ts:28

decryptMessage

Decrypt a PGP message and verify signature.
async function decryptMessage(
  message: Message<string>,
  senderPublicKey: PublicKey,
  recipientPrivateKey: PrivateKey
): Promise<{
  decrypted: string | Uint8Array;
  signatures: Signature[];
  signatureVerified: boolean;
}>
message
Message<string>
required
Encrypted message to decrypt
senderPublicKey
PublicKey
required
Sender’s public key for signature verification
recipientPrivateKey
PrivateKey
required
Recipient’s private key for decryption
Returns:
decrypted
string | Uint8Array
required
Decrypted message content
signatures
Signature[]
required
Array of signatures
signatureVerified
boolean
required
Whether the signature was successfully verified
Source: libs/skiff-crypto-v2/src/pgp/encrypt.ts:84

parseEncryptedMessage

Parse an armored encrypted message.
async function parseEncryptedMessage(
  encryptedText: string
): Promise<{
  encryptedMessage: Message;
  encryptedKeyIDs: KeyID[];
}>
encryptedText
string
required
Armored encrypted message text
Returns: Parsed message and key IDs Source: libs/skiff-crypto-v2/src/pgp/encrypt.ts:75

Web Key Directory (WKD)

fetchWKDKey

Fetch a recipient’s public key from Web Key Directory.
async function fetchWKDKey(
  emailAddress: string,
  originUrl: URL
): Promise<PublicKey | null>
emailAddress
string
required
Email address to query for PGP key
originUrl
URL
required
Base proxy URL for WKD queries
Returns: PublicKey if found, null otherwise Example:
import { fetchWKDKey } from 'skiff-crypto-v2';

const publicKey = await fetchWKDKey(
  '[email protected]',
  new URL('https://api.skiff.com')
);

if (publicKey) {
  console.log('Found PGP key for user');
}
Source: libs/skiff-crypto-v2/src/pgp/keyManagement.ts:160

computeWKDHash

Compute the WKD hash for an email address local part.
async function computeWKDHash(localPart: string): Promise<string>
localPart
string
required
Local part of email address (before @)
Returns: Z-Base-32 encoded SHA-1 hash (32 characters) Source: libs/skiff-crypto-v2/src/pgp/keyManagement.ts:128

verifyWKDHash

Verify that a provided hash matches the computed hash.
async function verifyWKDHash(
  localPart: string,
  providedHash: string
): Promise<boolean>
localPart
string
required
Local part of email address
providedHash
string
required
Hash to verify
Returns: Boolean indicating if hash is valid Source: libs/skiff-crypto-v2/src/pgp/keyManagement.ts:148

publishWKDKey

Convert a public key into WKD format for publishing.
async function publishWKDKey(armoredKey: string): Promise<Uint8Array>
armoredKey
string
required
Armored public key to publish
Returns: Binary representation of the public key Source: libs/skiff-crypto-v2/src/pgp/keyManagement.ts:217

Migration from v1

Basic Encryption Migration

Before (v1):
import { encryptSymmetric, createJSONWrapperDatagram } from 'skiff-crypto';

const datagram = createJSONWrapperDatagram('ddl://data');
const encrypted = encryptSymmetric(content, key, datagram);
After (v2):
import { encryptSymmetric, createProtoWrapperDatagramV2 } from 'skiff-crypto-v2';

const datagram = createProtoWrapperDatagramV2(
  'ddl://data',
  HeaderProto,
  BodyProto
);
const encrypted = encryptSymmetric(header, body, key, datagram);

Decryption Migration

Before (v1):
const decrypted = decryptSymmetric(encrypted, key, datagram);
// Returns: T (the content directly)
After (v2):
const { header, body, metadata } = decryptSymmetric(encrypted, key, datagram);
// Returns: { header: Header, body: Body, metadata: AADMeta }

Using Backward Compatible Functions

V2 re-exports all v1 functions for backward compatibility:
import {
  generatePublicPrivateKeyPair,
  generateSymmetricKey,
  stringEncryptAsymmetric,
  stringDecryptAsymmetric,
  generateHash
} from 'skiff-crypto-v2';

// These work exactly as in v1
const keypair = generatePublicPrivateKeyPair();
const hash = generateHash('data');

Complete Example

import {
  generatePGPKey,
  encryptMessage,
  decryptMessage,
  parseEncryptedMessage,
  readPublicKey
} from 'skiff-crypto-v2';

// Generate PGP keys for sender and recipient
const sender = await generatePGPKey([
  { name: 'Alice', email: '[email protected]' }
]);

const recipient = await generatePGPKey([
  { name: 'Bob', email: '[email protected]' }
]);

// Encrypt a message
const messageContent = new TextEncoder().encode('Secret message!');
const encryptedStream = await encryptMessage(
  messageContent,
  {
    name: 'Alice',
    email: '[email protected]',
    privateKey: await readPrivateKey(sender.privateKey),
    keyID: (await readPrivateKey(sender.privateKey)).getKeyIDs()[0]
  },
  [
    {
      name: 'Bob',
      email: '[email protected]',
      publicKey: await readPublicKey(recipient.publicKey),
      keyID: (await readPublicKey(recipient.publicKey)).getKeyIDs()[0]
    }
  ]
);

// Convert stream to string
const encryptedText = await getStringFromStream(encryptedStream);

// Decrypt the message
const { encryptedMessage } = await parseEncryptedMessage(encryptedText);
const { decrypted, signatureVerified } = await decryptMessage(
  encryptedMessage,
  await readPublicKey(sender.publicKey),
  await readPrivateKey(recipient.privateKey)
);

console.log('Decrypted:', new TextDecoder().decode(decrypted));
console.log('Signature verified:', signatureVerified);

Dependencies

  • @stablelib/chacha20poly1305: ChaCha20-Poly1305 AEAD encryption
  • openpgp: OpenPGP.js for PGP operations
  • protobufjs: Protocol Buffers support for datagrams
  • skiff-crypto: Re-exports core v1 functions
  • zbase32: Z-Base-32 encoding for WKD

Security Considerations

  • All PGP keys support ECC (Curve25519) and RSA (minimum 2048 bits)
  • ElGamal and DSA algorithms are explicitly rejected for security
  • WKD queries use CORS-safe proxy endpoints
  • Signature verification prevents re-interpretation attacks via context tags
  • ChaCha20-Poly1305 provides authenticated encryption with additional data (AEAD)

License

skiff-crypto-v2 is MIT licensed. It is part of the Skiff open-source project.

Build docs developers (and LLMs) love