Node.js 暗号化ストリームのbytesWrittenプロパティ:使い方と注意点

2025-06-01

cryptoStream.bytesWritten は、Node.jsの crypto モジュールによって作成されたストリーム(例えば、暗号化やハッシュ化を行うためのストリーム)が、これまでに書き込んだバイト数を示すプロパティです。

より具体的に説明すると:

  • .bytesWritten: このプロパティは、ストリームに対して stream.write() メソッドが呼び出され、実際にデータが処理のために書き込まれた総バイト数を追跡します。つまり、ストリームが内部的な処理のために受け取ったデータの量をバイト単位で表します。

  • cryptoStream: これは、crypto.createCipher(), crypto.createDecipher(), crypto.createHash(), crypto.createHmac(), crypto.createSign(), crypto.createVerify(), crypto.createGzip(), crypto.createDeflate() などの関数によって生成されたストリームオブジェクトを指します。これらのストリームは、データの暗号化、復号化、ハッシュ値の計算、署名、検証、圧縮、解凍などの処理をパイプライン的に行うために使用されます。

重要な点

  • 読み取り可能なストリーム(例えば、ファイルからの読み込みストリーム)から cryptoStream にパイプ処理している場合、cryptoStream.bytesWritten は読み取り可能なストリームから cryptoStream に書き込まれたデータの量を表します。
  • ストリームがまだデータを処理中であっても、bytesWritten の値は随時更新されます。
  • このプロパティは、ストリームがデータを処理する過程で、どれだけの入力データがストリームに渡されたかを監視するのに役立ちます。
  • cryptoStream.bytesWritten は、処理後の出力データのバイト数とは限りません。例えば、暗号化ストリームの場合、入力された平文のバイト数と出力される暗号文のバイト数は異なる可能性があります。圧縮ストリームの場合も同様です。

簡単な例

const crypto = require('crypto');
const fs = require('fs');

const inputFile = 'input.txt';
const outputFile = 'output.enc';
const key = 'your-secret-key';
const algorithm = 'aes-256-cbc';

const readStream = fs.createReadStream(inputFile);
const cipher = crypto.createCipheriv(algorithm, key, crypto.randomBytes(16));
const writeStream = fs.createWriteStream(outputFile);

readStream.pipe(cipher).pipe(writeStream);

cipher.on('data', () => {
  console.log(`暗号化ストリームに書き込まれたバイト数: ${cipher.bytesWritten}`);
});

writeStream.on('finish', () => {
  console.log('暗号化完了');
  console.log(`最終的な暗号化ストリームに書き込まれたバイト数: ${cipher.bytesWritten}`);
});

この例では、ファイル input.txt の内容が暗号化され、output.enc に書き込まれます。cipher.on('data', ...) の部分で、データが暗号化ストリームに書き込まれるたびに cipher.bytesWritten の値が出力されます。writeStream.on('finish', ...) では、暗号化処理が完了した時点での総書き込みバイト数が出力されます。



bytesWritten の値が期待通りにならない

  • トラブルシューティング

    • 処理完了後の値を確認
      最終的なバイト数を確認したい場合は、書き込み可能ストリームの 'finish' イベントや、読み取り可能ストリームの 'end' イベントのリスナー内で bytesWritten を確認します。
    • 'data' イベントの監視
      データの流れを詳細に把握したい場合は、cryptoStream'data' イベントを監視し、各チャンクが書き込まれるたびに bytesWritten の値を確認します。
    • エラーイベントの監視
      ストリームのエラーを捕捉するために 'error' イベントのリスナーを追加し、エラー発生時に bytesWritten の値を確認して、書き込みが中断された時点を把握します。
    • ログ出力の活用
      処理の各段階で関連する情報をログに出力し、データの流れと bytesWritten の変化を追跡します。
    • ストリームがまだ完全に処理を終えていない
      bytesWritten は処理が進行するにつれて増加します。処理完了前に値を確認すると、最終的な値よりも小さくなります。
    • データのチャンクサイズの影響
      stream.write() に渡されるデータのチャンクサイズによって、'data' イベントの発生回数や bytesWritten の増加量が変動します。
    • パイプ処理のタイミング
      パイプ (pipe()) を使用している場合、データの流れが非同期であるため、特定の時点での bytesWritten の値は、読み取り元ストリームの進行状況や cryptoStream の処理速度に依存します。
    • エラー処理の欠如
      ストリーム処理中にエラーが発生した場合、意図した量のデータが書き込まれない可能性がありますが、bytesWritten はそれまでに書き込まれたバイト数を反映します。

bytesWritten の値を利用したロジックの誤り



例1: 暗号化処理における書き込みバイト数の監視

この例では、ファイルの内容を暗号化しながら、暗号化ストリームに書き込まれたバイト数を監視します。

const crypto = require('crypto');
const fs = require('fs');

const inputFile = 'input.txt';
const outputFile = 'output.enc';
const key = 'your-secret-key';
const algorithm = 'aes-256-cbc';

const readStream = fs.createReadStream(inputFile);
const cipher = crypto.createCipheriv(algorithm, key, crypto.randomBytes(16));
const writeStream = fs.createWriteStream(outputFile);

readStream.pipe(cipher).pipe(writeStream);

cipher.on('data', (chunk) => {
  console.log(`暗号化されたデータのチャンク (${chunk.length} バイト) が処理されました。`);
  console.log(`暗号化ストリームに書き込まれた累計バイト数: ${cipher.bytesWritten}`);
});

writeStream.on('finish', () => {
  console.log('暗号化が完了しました。');
  console.log(`最終的な暗号化ストリームへの書き込みバイト数: ${cipher.bytesWritten}`);
});

writeStream.on('error', (err) => {
  console.error('書き込みエラー:', err);
});

readStream.on('error', (err) => {
  console.error('読み込みエラー:', err);
});

cipher.on('error', (err) => {
  console.error('暗号化エラー:', err);
});

説明

  1. fs.createReadStream(inputFile) で入力ファイルからの読み取りストリームを作成します。
  2. crypto.createCipheriv(algorithm, key, crypto.randomBytes(16)) で暗号化ストリーム (cipher) を作成します。
  3. fs.createWriteStream(outputFile) で出力ファイルへの書き込みストリームを作成します。
  4. readStream.pipe(cipher).pipe(writeStream) で、読み取ったデータを暗号化ストリームを通して書き込みストリームにパイプ処理します。
  5. cipher.on('data', ...) イベントリスナーでは、暗号化されたデータのチャンクが処理されるたびに、そのチャンクのサイズ (chunk.length) と、その時点までの cipher.bytesWritten の値を出力します。これにより、暗号化ストリームがどれだけの入力データを処理したかをリアルタイムに確認できます。
  6. writeStream.on('finish', ...) イベントリスナーは、書き込み処理が完了したときに呼び出され、最終的な cipher.bytesWritten の値を出力します。
  7. 各ストリームのエラーイベント ('error') を監視し、エラーが発生した場合にログ出力するようにしています。

例2: ハッシュ化処理における書き込みバイト数の監視

この例では、文字列のハッシュ値を計算しながら、ハッシュ化ストリームに書き込まれたバイト数を監視します。

const crypto = require('crypto');

const data = 'This is some data to hash.';
const hashAlgorithm = 'sha256';

const hash = crypto.createHash(hashAlgorithm);
hash.update(data, 'utf8');
const hexDigest = hash.digest('hex');

console.log(`ハッシュ値 (${hashAlgorithm}): ${hexDigest}`);
console.log(`ハッシュストリームに書き込まれたバイト数 (update): ${hash.bytesWritten}`);

// ストリームとしても利用可能
const hashStream = crypto.createHash(hashAlgorithm);
hashStream.on('data', (chunk) => {
  console.log(`ハッシュストリームに書き込まれたデータのチャンク (${chunk.length} バイト)`);
  console.log(`ハッシュストリームの現在の書き込みバイト数: ${hashStream.bytesWritten}`);
});

hashStream.on('end', () => {
  console.log(`ハッシュ計算完了。最終的な書き込みバイト数: ${hashStream.bytesWritten}`);
  console.log(`最終的なハッシュ値 (stream): ${hashStream.digest('hex')}`);
});

hashStream.write('Another piece ');
hashStream.write('of data.');
hashStream.end();

説明

  1. 最初の部分では、crypto.createHash(hashAlgorithm) でハッシュオブジェクトを作成し、hash.update(data, 'utf8') でデータを更新してから hash.digest('hex') で最終的なハッシュ値を取得しています。この時点での hash.bytesWritten は、update() メソッドに渡されたデータのバイト数を表します。
  2. 次の部分では、ハッシュオブジェクトをストリームとして利用しています。
  3. hashStream.on('data', ...) イベントリスナーは、write() メソッドでデータが書き込まれるたびに、書き込まれたデータのチャンクサイズと現在の hashStream.bytesWritten の値を出力します。
  4. hashStream.on('end', ...) イベントリスナーは、hashStream.end() が呼び出されてストリームが終了したときに、最終的な hashStream.bytesWritten の値と計算されたハッシュ値を出力します。

例3: 圧縮処理における書き込みバイト数の監視

この例では、データをgzip圧縮しながら、圧縮ストリームに書き込まれたバイト数を監視します。

const crypto = require('crypto');
const zlib = require('zlib');
const fs = require('fs');

const inputFile = 'large_input.txt';
const outputFile = 'output.gz';

const readStream = fs.createReadStream(inputFile);
const gzip = zlib.createGzip();
const writeStream = fs.createWriteStream(outputFile);

readStream.pipe(gzip).pipe(writeStream);

gzip.on('data', (chunk) => {
  console.log(`圧縮されたデータのチャンク (${chunk.length} バイト) が処理されました。`);
  console.log(`gzipストリームに書き込まれた累計バイト数: ${gzip.bytesWritten}`);
});

writeStream.on('finish', () => {
  console.log('圧縮が完了しました。');
  console.log(`最終的な gzip ストリームへの書き込みバイト数: ${gzip.bytesWritten}`);
});

writeStream.on('error', (err) => {
  console.error('書き込みエラー:', err);
});

readStream.on('error', (err) => {
  console.error('読み込みエラー:', err);
});

gzip.on('error', (err) => {
  console.error('圧縮エラー:', err);
});

説明

この例は暗号化の例と似ていますが、crypto.createCipheriv() の代わりに zlib.createGzip() を使用してgzip圧縮ストリームを作成しています。gzip.on('data', ...) イベントリスナーで、圧縮ストリームに書き込まれたバイト数を監視しています。gzip圧縮の場合、入力データよりも出力データ(圧縮後のデータ)のサイズが小さくなることが一般的ですが、gzip.bytesWritten はあくまで圧縮ストリームが受け取った(つまり、読み取りストリームからパイプされた)元のデータのバイト数を追跡します。出力される圧縮データのサイズは、出力ストリーム (writeStream) の bytesWritten プロパティで確認できますが、cryptoStreambytesWritten とは異なることに注意してください。



'data' イベントとチャンクサイズの累積

cryptoStream (および他のストリーム) は、処理されたデータのチャンクごとに 'data' イベントを発行します。このイベントリスナー内で、受け取ったチャンクのサイズ (chunk.length) を累積することで、実質的に bytesWritten と同様の情報を得ることができます。

const crypto = require('crypto');
const fs = require('fs');

const inputFile = 'input.txt';
const outputFile = 'output.enc';
const key = 'your-secret-key';
const algorithm = 'aes-256-cbc';

const readStream = fs.createReadStream(inputFile);
const cipher = crypto.createCipheriv(algorithm, key, crypto.randomBytes(16));
const writeStream = fs.createWriteStream(outputFile);

let processedBytes = 0;

cipher.on('data', (chunk) => {
  processedBytes += chunk.length;
  console.log(`処理済みバイト数 (data イベント): ${processedBytes}`);
});

readStream.pipe(cipher).pipe(writeStream);

writeStream.on('finish', () => {
  console.log(`最終的な処理済みバイト数 (data イベント): ${processedBytes}`);
  console.log(`cryptoStream.bytesWritten (finish イベント): ${cipher.bytesWritten}`);
});

// エラー処理は省略

利点

  • チャンクごとの処理ロジックを実装する際に、同時にバイト数を追跡できます。
  • 'data' イベントは、データが実際に処理されて次のストリームに渡されるタイミングで発生するため、より細かい粒度で処理状況を把握できます。

欠点

  • bytesWritten プロパティのように直接的な値ではないため、自分で累積変数を管理する必要があります。

パイプ処理の終了イベントと入力ストリームのサイズ

パイプ処理を使用している場合、cryptoStream にパイプされる入力ストリーム(例えば、fs.ReadStream)のサイズを事前に知っていれば、処理が完了した時点で cryptoStream.bytesWritten の値が入力ストリームのサイズと一致する(はずである)ことを期待できます。ただし、変換処理(暗号化、圧縮など)によっては、入力と出力のバイト数が異なる場合があることに注意が必要です。

const crypto = require('crypto');
const fs = require('fs');

const inputFile = 'input.txt';
const outputFile = 'output.enc';
const key = 'your-secret-key';
const algorithm = 'aes-256-cbc';

fs.stat(inputFile, (err, stats) => {
  if (err) {
    console.error('ファイル情報の取得エラー:', err);
    return;
  }

  const inputFileSize = stats.size;
  const readStream = fs.createReadStream(inputFile);
  const cipher = crypto.createCipheriv(algorithm, key, crypto.randomBytes(16));
  const writeStream = fs.createWriteStream(outputFile);

  readStream.pipe(cipher).pipe(writeStream);

  writeStream.on('finish', () => {
    console.log(`入力ファイルサイズ: ${inputFileSize} バイト`);
    console.log(`cryptoStream.bytesWritten (finish イベント): ${cipher.bytesWritten} バイト`);
    // 通常、暗号化後のサイズは異なる可能性があります
  });

  // エラー処理は省略
});

利点

  • 入力データの全体像を把握するのに役立ちます。

欠点

  • 入力ストリームがサイズを事前に特定できない場合(例えば、ネットワークストリーム)、この方法は適用できません。
  • cryptoStream が実際に処理したバイト数と完全に一致するとは限りません(特に変換処理の場合)。