CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-sodium-native

Low level bindings for libsodium cryptographic library

Pending
Overview
Eval results
Files

secretstream.mddocs/

Secret Streams

Streaming authenticated encryption using XChaCha20-Poly1305 for encrypting sequences of messages with automatic key rotation and message ordering.

Capabilities

Key Generation

Generate keys for secret stream operations.

/**
 * Generate random key for secret stream encryption
 * @param k - Output buffer for key (must be KEYBYTES long)
 */
function crypto_secretstream_xchacha20poly1305_keygen(k: Buffer): void;

Usage Example:

const sodium = require('sodium-native');

// Generate key for secret stream
const key = Buffer.alloc(sodium.crypto_secretstream_xchacha20poly1305_KEYBYTES);
sodium.crypto_secretstream_xchacha20poly1305_keygen(key);

Push Stream Initialization

Initialize a secret stream for encryption (push mode).

/**
 * Initialize secret stream for encryption
 * @param state - Output buffer for stream state (must be STATEBYTES long)
 * @param header - Output buffer for stream header (must be HEADERBYTES long)
 * @param k - Key buffer (must be KEYBYTES long)
 * @throws Error if initialization fails or buffer sizes incorrect
 */
function crypto_secretstream_xchacha20poly1305_init_push(
  state: Buffer,
  header: Buffer,
  k: Buffer
): void;

Pull Stream Initialization

Initialize a secret stream for decryption (pull mode).

/**
 * Initialize secret stream for decryption
 * @param state - Output buffer for stream state (must be STATEBYTES long)
 * @param header - Stream header from push initialization (must be HEADERBYTES long)
 * @param k - Key buffer (must be KEYBYTES long)
 * @throws Error if initialization fails or buffer sizes incorrect
 */
function crypto_secretstream_xchacha20poly1305_init_pull(
  state: Buffer,
  header: Buffer,
  k: Buffer
): void;

Usage Example:

const sodium = require('sodium-native');

// Initialize encryption stream
const pushState = Buffer.alloc(sodium.crypto_secretstream_xchacha20poly1305_STATEBYTES);
const header = Buffer.alloc(sodium.crypto_secretstream_xchacha20poly1305_HEADERBYTES);

sodium.crypto_secretstream_xchacha20poly1305_init_push(pushState, header, key);

// Initialize decryption stream  
const pullState = Buffer.alloc(sodium.crypto_secretstream_xchacha20poly1305_STATEBYTES);
sodium.crypto_secretstream_xchacha20poly1305_init_pull(pullState, header, key);

Push (Encryption)

Encrypt and authenticate a message chunk in the stream.

/**
 * Encrypt message chunk in secret stream
 * @param state - Stream state buffer from init_push
 * @param c - Output buffer for ciphertext (must be m.length + ABYTES)
 * @param m - Message buffer to encrypt
 * @param ad - Optional additional data buffer (can be null)
 * @param tag - Message tag indicating message type/position
 * @returns Number of bytes written to ciphertext buffer
 * @throws Error if encryption fails
 */
function crypto_secretstream_xchacha20poly1305_push(
  state: Buffer,
  c: Buffer,
  m: Buffer,
  ad: Buffer | null,
  tag: number
): number;

Pull (Decryption)

Decrypt and verify a message chunk from the stream.

/**
 * Decrypt and verify message chunk from secret stream
 * @param state - Stream state buffer from init_pull
 * @param m - Output buffer for plaintext (must be c.length - ABYTES)
 * @param tag - Output buffer for message tag (must be TAGBYTES long)
 * @param c - Ciphertext buffer to decrypt
 * @param ad - Optional additional data buffer (can be null)
 * @returns Number of bytes written to plaintext buffer
 * @throws Error if decryption or verification fails
 */
function crypto_secretstream_xchacha20poly1305_pull(
  state: Buffer,
  m: Buffer,
  tag: Buffer,
  c: Buffer,
  ad: Buffer | null
): number;

Usage Example:

const sodium = require('sodium-native');

// Encrypt message chunks
const message1 = Buffer.from('First message');
const message2 = Buffer.from('Second message');
const message3 = Buffer.from('Final message');

// Encrypt first message
const ciphertext1 = Buffer.alloc(message1.length + sodium.crypto_secretstream_xchacha20poly1305_ABYTES);
const len1 = sodium.crypto_secretstream_xchacha20poly1305_push(
  pushState,
  ciphertext1,
  message1,
  null,
  sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE
);

// Encrypt second message  
const ciphertext2 = Buffer.alloc(message2.length + sodium.crypto_secretstream_xchacha20poly1305_ABYTES);
const len2 = sodium.crypto_secretstream_xchacha20poly1305_push(
  pushState,
  ciphertext2,
  message2,
  null,
  sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE
);

// Encrypt final message
const ciphertext3 = Buffer.alloc(message3.length + sodium.crypto_secretstream_xchacha20poly1305_ABYTES);
const len3 = sodium.crypto_secretstream_xchacha20poly1305_push(
  pushState,
  ciphertext3,
  message3,
  null,
  sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL
);

// Decrypt message chunks
const plaintext1 = Buffer.alloc(ciphertext1.length - sodium.crypto_secretstream_xchacha20poly1305_ABYTES);
const tag1 = Buffer.alloc(1);
sodium.crypto_secretstream_xchacha20poly1305_pull(pullState, plaintext1, tag1, ciphertext1, null);

console.log('Decrypted:', plaintext1.toString());
console.log('Tag:', tag1[0]); // Should be TAG_MESSAGE

Key Rotation

Rotate the stream key to provide forward secrecy.

/**
 * Rotate stream key for forward secrecy
 * @param state - Stream state buffer to rekey
 */
function crypto_secretstream_xchacha20poly1305_rekey(state: Buffer): void;

Usage Example:

const sodium = require('sodium-native');

// Rotate key after processing sensitive data
sodium.crypto_secretstream_xchacha20poly1305_rekey(pushState);

// Messages encrypted after rekey cannot be decrypted with old state
const sensitiveMessage = Buffer.from('Top secret data');
const encryptedSensitive = Buffer.alloc(sensitiveMessage.length + sodium.crypto_secretstream_xchacha20poly1305_ABYTES);

sodium.crypto_secretstream_xchacha20poly1305_push(
  pushState,
  encryptedSensitive,
  sensitiveMessage,
  null,
  sodium.crypto_secretstream_xchacha20poly1305_TAG_REKEY
);

Constants

// State buffer size in bytes
const crypto_secretstream_xchacha20poly1305_STATEBYTES: number;

// Authentication tag size in bytes
const crypto_secretstream_xchacha20poly1305_ABYTES: number;

// Stream header size in bytes
const crypto_secretstream_xchacha20poly1305_HEADERBYTES: number;

// Key size in bytes
const crypto_secretstream_xchacha20poly1305_KEYBYTES: number;

// Message tag size in bytes
const crypto_secretstream_xchacha20poly1305_TAGBYTES: number;

// Maximum message size in bytes
const crypto_secretstream_xchacha20poly1305_MESSAGEBYTES_MAX: number;

// Message tags for different message types
const crypto_secretstream_xchacha20poly1305_TAG_MESSAGE: number;
const crypto_secretstream_xchacha20poly1305_TAG_PUSH: number;
const crypto_secretstream_xchacha20poly1305_TAG_REKEY: number;
const crypto_secretstream_xchacha20poly1305_TAG_FINAL: number;

Message Tags

Secret streams use tags to indicate message types and stream state:

  • TAG_MESSAGE: Regular message in the stream
  • TAG_PUSH: End of a push sequence (commit point)
  • TAG_REKEY: Key rotation occurred (forward secrecy)
  • TAG_FINAL: Final message in the stream

Security Considerations

  • Forward Secrecy: Use TAG_REKEY and rekey() function to prevent decryption of future messages if state is compromised.
  • Message Ordering: Secret streams enforce message ordering and prevent replay attacks.
  • Stream Integrity: Each message is authenticated within the context of the entire stream.
  • Key Management: Store and transmit the stream header securely alongside the first ciphertext.

Common Patterns

Secure File Streaming

const sodium = require('sodium-native');
const fs = require('fs');

class SecureFileStream {
  constructor(key) {
    this.key = key || Buffer.alloc(sodium.crypto_secretstream_xchacha20poly1305_KEYBYTES);
    if (!key) {
      sodium.crypto_secretstream_xchacha20poly1305_keygen(this.key);
    }
  }
  
  encryptFile(inputFile, outputFile) {
    const state = Buffer.alloc(sodium.crypto_secretstream_xchacha20poly1305_STATEBYTES);
    const header = Buffer.alloc(sodium.crypto_secretstream_xchacha20poly1305_HEADERBYTES);
    
    sodium.crypto_secretstream_xchacha20poly1305_init_push(state, header, this.key);
    
    const input = fs.createReadStream(inputFile, { highWaterMark: 4096 });
    const output = fs.createWriteStream(outputFile);
    
    // Write header first
    output.write(header);
    
    let isFirstChunk = true;
    let isLastChunk = false;
    
    input.on('data', (chunk) => {
      const ciphertext = Buffer.alloc(chunk.length + sodium.crypto_secretstream_xchacha20poly1305_ABYTES);
      
      // Determine tag based on position
      let tag = sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
      if (isFirstChunk) {
        tag = sodium.crypto_secretstream_xchacha20poly1305_TAG_PUSH;
        isFirstChunk = false;
      }
      
      const len = sodium.crypto_secretstream_xchacha20poly1305_push(
        state, ciphertext, chunk, null, tag
      );
      
      output.write(ciphertext.subarray(0, len));
    });
    
    input.on('end', () => {
      // Write final empty message with TAG_FINAL  
      const finalCiphertext = Buffer.alloc(sodium.crypto_secretstream_xchacha20poly1305_ABYTES);
      const len = sodium.crypto_secretstream_xchacha20poly1305_push(
        state, finalCiphertext, Buffer.alloc(0), null,
        sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL
      );
      
      output.write(finalCiphertext.subarray(0, len));
      output.end();
    });
  }
  
  decryptFile(inputFile, outputFile) {
    const encryptedData = fs.readFileSync(inputFile);
    const header = encryptedData.subarray(0, sodium.crypto_secretstream_xchacha20poly1305_HEADERBYTES);
    
    const state = Buffer.alloc(sodium.crypto_secretstream_xchacha20poly1305_STATEBYTES);
    sodium.crypto_secretstream_xchacha20poly1305_init_pull(state, header, this.key);
    
    const output = fs.createWriteStream(outputFile);
    let offset = sodium.crypto_secretstream_xchacha20poly1305_HEADERBYTES;
    
    while (offset < encryptedData.length) {
      // Read chunk length (simplified - real implementation needs proper framing)
      const chunkSize = Math.min(4096 + sodium.crypto_secretstream_xchacha20poly1305_ABYTES,
                                encryptedData.length - offset);
      
      const ciphertext = encryptedData.subarray(offset, offset + chunkSize);
      const plaintext = Buffer.alloc(ciphertext.length - sodium.crypto_secretstream_xchacha20poly1305_ABYTES);
      const tag = Buffer.alloc(1);
      
      const len = sodium.crypto_secretstream_xchacha20poly1305_pull(
        state, plaintext, tag, ciphertext, null
      );
      
      if (len > 0) {
        output.write(plaintext.subarray(0, len));
      }
      
      // Check for final tag
      if (tag[0] === sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL) {
        break;
      }
      
      offset += chunkSize;
    }
    
    output.end();
  }
}

Network Protocol with Forward Secrecy

const sodium = require('sodium-native');

class SecureProtocol {
  constructor() {
    this.sendState = null;
    this.receiveState = null;
    this.messageCount = 0;
  }
  
  initializeSession(key, isInitiator = true) {
    const header = Buffer.alloc(sodium.crypto_secretstream_xchacha20poly1305_HEADERBYTES);
    
    if (isInitiator) {
      this.sendState = Buffer.alloc(sodium.crypto_secretstream_xchacha20poly1305_STATEBYTES);
      sodium.crypto_secretstream_xchacha20poly1305_init_push(this.sendState, header, key);
      return header; // Send to peer
    } else {
      this.receiveState = Buffer.alloc(sodium.crypto_secretstream_xchacha20poly1305_STATEBYTES);
      sodium.crypto_secretstream_xchacha20poly1305_init_pull(this.receiveState, header, key);
    }
  }
  
  sendMessage(message, isImportant = false) {
    if (!this.sendState) throw new Error('Send state not initialized');
    
    const ciphertext = Buffer.alloc(message.length + sodium.crypto_secretstream_xchacha20poly1305_ABYTES);
    
    // Use different tags for message importance
    const tag = isImportant ? 
      sodium.crypto_secretstream_xchacha20poly1305_TAG_PUSH :
      sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
    
    const len = sodium.crypto_secretstream_xchacha20poly1305_push(
      this.sendState, ciphertext, message, null, tag
    );
    
    this.messageCount++;
    
    // Rotate key every 1000 messages for forward secrecy
    if (this.messageCount % 1000 === 0) {
      sodium.crypto_secretstream_xchacha20poly1305_rekey(this.sendState);
    }
    
    return ciphertext.subarray(0, len);
  }
  
  receiveMessage(ciphertext) {
    if (!this.receiveState) throw new Error('Receive state not initialized');
    
    const plaintext = Buffer.alloc(ciphertext.length - sodium.crypto_secretstream_xchacha20poly1305_ABYTES);
    const tag = Buffer.alloc(1);
    
    const len = sodium.crypto_secretstream_xchacha20poly1305_pull(
      this.receiveState, plaintext, tag, ciphertext, null
    );
    
    // Handle different message tags
    switch (tag[0]) {
      case sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE:
        return { message: plaintext.subarray(0, len), type: 'regular' };
      case sodium.crypto_secretstream_xchacha20poly1305_TAG_PUSH:
        return { message: plaintext.subarray(0, len), type: 'important' };
      case sodium.crypto_secretstream_xchacha20poly1305_TAG_REKEY:
        sodium.crypto_secretstream_xchacha20poly1305_rekey(this.receiveState);
        return { message: plaintext.subarray(0, len), type: 'rekey' };
      case sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL:
        return { message: plaintext.subarray(0, len), type: 'final' };
      default:
        throw new Error('Unknown message tag');
    }
  }
}

Install with Tessl CLI

npx tessl i tessl/npm-sodium-native

docs

aead.md

auth.md

box.md

ed25519.md

hash.md

index.md

kdf.md

kx.md

memory.md

pwhash.md

random.md

secretbox.md

secretstream.md

shorthash.md

sign.md

stream.md

tile.json