📚

JavaScript(TS)用の準同型暗号ライブラリ"node-seal"のNext.jsを使用した実装

2024/02/26に公開

はじめに

ETHGlobalで準同型暗号を実装する必要があり、いくつかライブラリを試して動かしたところnode-sealというので簡単に実装することができたので、Next.js(Typescript)を使用した実装を紹介します。
https://github.com/s0l0ist/node-seal

node-sealについてはこちらの記事を参照ください。
https://qiita.com/kenmaro/items/15b300333b0854f56bbc

セットアップ

  1. まずは npx create-next-app@latest --typescript でNextプロジェクトを立ち上げます。ドキュメントをご参照ください。
    https://nextjs-ja-translation-docs.vercel.app/docs/getting-started

  2. 次に node-seal をインストールします。

npm install node-seal
  1. プロジェクトのルートに data ディレクトリを作成しておきます。
    今回はこのdataディレクトリに鍵や暗号化したファイルなどを置いていきます。

  2. node-sealのセットアップをします。/utils/sealFunction.ts/utils/sealInitialize.tsを作成します。

  • /utils/sealFunction.ts
    鍵を保存したり読み込んだりします。dataディレクトリに保存したり、そこにあるものを読み込みます。
sealFunctions.ts
import { writeFile, readFile } from 'fs/promises';
import path from 'path';

// 鍵を保存する関数
export async function saveKeys(publicKey: any, secretKey: any) {
  const keys = {
    publicKey: publicKey.save(),
    secretKey: secretKey.save()
  };
  const filePath = path.join(process.cwd(), 'data', 'keys.json');
  await writeFile(filePath, JSON.stringify(keys), 'utf8');
}

// 鍵を読み込む関数
 export async function loadKeys(seal: any, context: any) {
  const filePath = path.join(process.cwd(), 'data', 'keys.json');
  try {
    const data = await readFile(filePath, 'utf8');
    const keys = JSON.parse(data);
    const publicKey = seal.PublicKey();
    const secretKey = seal.SecretKey();
    publicKey.load(context, keys.publicKey);
    secretKey.load(context, keys.secretKey);
    return { publicKey, secretKey };
  } catch (error) {
    console.error('Error loading keys:', error);
    return null;
  }
}
  • /utils/sealInitialize.ts
    node-sealの設定を初期化していきます。
sealInitialize.ts
import SEAL from 'node-seal';
import { saveKeys, loadKeys } from './sealFunctions';

export async function initializeSeal() {
    if (!process.env.POLY_MODULUS_DEGREE || !process.env.COEFF_MODULUS_SIZES || !process.env.PLAIN_MODULUS_SIZE) {
        throw new Error('Environment variables for SEAL configuration are not set properly.');
      }
  // node-seal ライブラリのインスタンスを非同期で初期化
  const seal = await SEAL();

  // 暗号スキームのタイプを BFV に設定
  const schemeType = seal.SchemeType.bfv;

  // セキュリティレベルを 128 ビットに設定
  const securityLevel = seal.SecurityLevel.tc128;

  // ポリモジュラス次数を設定
  const polyModulusDegree = parseInt(process.env.POLY_MODULUS_DEGREE, 10);
  // 係数モジュラスのビットサイズを設定
  const bitSizes = process.env.COEFF_MODULUS_SIZES.split(',').map(Number);
  // 平文モジュラスのビットサイズを設定
  const bitSize = parseInt(process.env.PLAIN_MODULUS_SIZE, 10);

  // 暗号化パラメータの設定
  const encParms = seal.EncryptionParameters(schemeType);
  encParms.setPolyModulusDegree(polyModulusDegree);
  encParms.setCoeffModulus(seal.CoeffModulus.Create(polyModulusDegree, Int32Array.from(bitSizes)));
  encParms.setPlainModulus(seal.PlainModulus.Batching(polyModulusDegree, bitSize));

  // 暗号化パラメータを使用して暗号化コンテキストを生成
  const context = seal.Context(encParms, true, securityLevel);

  // コンテキストのパラメータが正しく設定されたかを確認
  if (!context.parametersSet()) {
    throw new Error('Could not set the parameters in the given context. Please try different encryption parameters.');
  }

let keys = await loadKeys(seal, context);
if (!keys) {
  const keyGenerator = seal.KeyGenerator(context);
  let publicKey = keyGenerator.createPublicKey();
  let secretKey = keyGenerator.secretKey();

  await saveKeys(publicKey, secretKey);

  // 保存した鍵を再読み込み
  keys = await loadKeys(seal, context);
  if (!keys) {
    throw new Error('鍵の保存後の読み込みに失敗しました。');
  }
}

// この時点で keys は null ではないことが保証されています
const publicKey = keys.publicKey;
const secretKey = keys.secretKey;

const encryptor = seal.Encryptor(context, publicKey);
const decryptor = seal.Decryptor(context, secretKey);

  // 評価器(暗号文同士の演算を行うためのオブジェクト)を生成
  const evaluator = seal.Evaluator(context);

  // バッチエンコーダー(複数の数値を一つの暗号文にエンコード・デコードするためのオブジェクト)を生成
  const batchEncoder = seal.BatchEncoder(context);

  // 必要なインスタンスを返す
  return { seal, encryptor, decryptor, evaluator, batchEncoder, publicKey, secretKey, context };
}

実装

準備が整ったところで実際に、値を暗号化したり、暗号化したまま計算したりしてみましょう。
下記全て /pages/api/ 配下に作成していきます。

encrypt.ts

引数{number1, number2}で受け取った2つの値を暗号化し、cipherText1.txtとcipherText2.txtというファイルで/dataディレクトリ配下に保存します。

encrypt.ts
// pages/api/encrypt.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { writeFile } from 'fs/promises';
import path from 'path';
import { initializeSeal } from '../../utils/sealInitialize';

type EncryptResponseData = {
  cipherText1?: string;
  cipherText2?: string;
  error?: string;
}

export default async function handler(req: NextApiRequest, res: NextApiResponse<EncryptResponseData>) {
  if (req.method === 'POST') {
    try {
      const { encryptor, batchEncoder, decryptor } = await initializeSeal();
      const { number1, number2 } = req.body;
      console.log('number1:', number1);
      console.log('number2:', number2);

      // 数値を暗号化
      const encodedNumber1 = batchEncoder.encode(Int32Array.from([number1]));
      const encodedNumber2 = batchEncoder.encode(Int32Array.from([number2]));
      // Encode処理がvoidでないことを確認
      if (!encodedNumber1 || !encodedNumber2) {
          throw new Error('Encode failed');
      }

      // 暗号文をBase64文字列として保存
      const cipherText1 = encryptor.encrypt(encodedNumber1);
      const cipherText2 = encryptor.encrypt(encodedNumber2);

      // 暗号文がvoidでないことを確認
      if (!cipherText1 || !cipherText2) {
          throw new Error('Encryption failed');
      }
      const cipherTextBase64_1 = cipherText1.save();
      const cipherTextBase64_2 = cipherText2.save();

      // ファイルに保存するパスを定義
      const filePath1 = path.join(process.cwd(), 'data', 'cipherText1.txt');
      const filePath2 = path.join(process.cwd(), 'data', 'cipherText2.txt');

      // ファイルに暗号文を非同期で書き込む
      await writeFile(filePath1, cipherTextBase64_1, 'utf8');
      await writeFile(filePath2, cipherTextBase64_2, 'utf8');

      res.status(200).json({ cipherText1: cipherTextBase64_1, cipherText2: cipherTextBase64_2 });
    } catch (error) {
      console.error(error);
      res.status(500).json({ error: 'Internal Server Error' });
    }
  } else {
    res.setHeader('Allow', ['POST']);
    res.status(405).end(`Method ${req.method} Not Allowed`);
  }
}

evaluate.ts

加算、乗算、減算などのさまざまな演算を暗号化したまま行います。演算したい種類のものを選び適宜コメントアウトしたり、コメントしたりして常に1つを選んでください。ここの部分です。

いずれか一つコメントを外す
// 加算
const accumulatedCipherText = seal.CipherText();
evaluator.add(cipherTextLoad_1, cipherTextLoad_2, accumulatedCipherText);

// 乗算
// const accumulatedCipherText = seal.CipherText();
// evaluator.multiply(cipherTextLoad_1, cipherTextLoad_2, accumulatedCipherText);

// 減算
// const accumulatedCipherText = seal.CipherText();
// evaluator.sub(cipherTextLoad_1, cipherTextLoad_2, accumulatedCipherText);

// 累乗
// const accumulatedCipherText = seal.CipherText();
// evaluator.square(cipherTextLoad_1, cipherTextLoad_2, accumulatedCipherText);
evaluate.ts
// pages/api/add.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { initializeSeal } from '../../utils/sealInitialize';
import { readFile } from 'fs/promises';
import path from 'path'
import { writeFile } from 'fs/promises';

type AddResponseData = {
  cipherText?: string;
  error?: string;
}

export default async function handler(req: NextApiRequest, res: NextApiResponse<AddResponseData>) {
  if (req.method === 'POST') {
    try {
      const { evaluator, encryptor, decryptor, batchEncoder, seal, context } = await initializeSeal();

      // ファイルから暗号文を読み込む
      const filePath1 = path.join(process.cwd(), 'data', 'cipherText1.txt');
      const cipherTextBase64_1 = await readFile(filePath1, 'utf8')
      const filePath2 = path.join(process.cwd(), 'data', 'cipherText2.txt');
      const cipherTextBase64_2 = await readFile(filePath2, 'utf8')

      // 暗号文をロード
      const cipherTextLoad_1 = seal.CipherText();
      const cipherTextLoad_2 = seal.CipherText();
      cipherTextLoad_1.load(context, cipherTextBase64_1);
      cipherTextLoad_2.load(context, cipherTextBase64_2);
      
      // 加算
      const accumulatedCipherText = seal.CipherText();
      evaluator.add(cipherTextLoad_1, cipherTextLoad_2, accumulatedCipherText);

      // 乗算
      // const accumulatedCipherText = seal.CipherText();
      // evaluator.multiply(cipherTextLoad_1, cipherTextLoad_2, accumulatedCipherText);

      // 減算
      // const accumulatedCipherText = seal.CipherText();
      // evaluator.sub(cipherTextLoad_1, cipherTextLoad_2, accumulatedCipherText);

      // 累乗
      // const accumulatedCipherText = seal.CipherText();
      // evaluator.square(cipherTextLoad_1, cipherTextLoad_2, accumulatedCipherText);

      // 加算結果の暗号文をBase64文字列として保存
      const cipherTextBase64 = accumulatedCipherText.save();
      const filePath = path.join(process.cwd(), 'data', 'accumulatedCipherText.txt');

      // ファイルに暗号文を非同期で書き込む
      await writeFile(filePath, cipherTextBase64, 'utf8')

      res.status(200).json({ cipherText: cipherTextBase64 });
    } catch (error) {
      console.error(error);
      res.status(500).json({ error: 'Internal Server Error' });
    }
  } else {
    res.setHeader('Allow', ['POST']);
    res.status(405).end(`Method ${req.method} Not Allowed`);
  }
}

decrypt.ts

evaluate.ts で計算した結果の暗号化されたデータを復号化します。data/ディレクトリ配下にあるaccumulatedCipherText.txt を復号化します。

decrypt.ts
// pages/api/decrypt.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { initializeSeal } from '../../utils/sealInitialize';
import { readFile } from 'fs/promises';
import path from 'path';

type DecryptResponseData = {
  number?: number;
  error?: string;
}

export default async function handler(req: NextApiRequest, res: NextApiResponse<DecryptResponseData>) {
  if (req.method === 'POST') {
    try {
      const { decryptor, batchEncoder, seal, context} = await initializeSeal();

      // ファイルから暗号文を読み込む
      const filePath = path.join(process.cwd(), 'data', 'accumulatedCipherText.txt');
      const cipherTextBase64 = await readFile(filePath, 'utf8');

      // 暗号文をロード
      const cipherTextLoad = seal.CipherText();
      cipherTextLoad.load(context, cipherTextBase64);
      
      // 復号化
      const decodedResult = decryptor.decrypt(cipherTextLoad);
      // decodedResultがvoidでないことを確認
      if (!decodedResult) {
          throw new Error('Decryption failed');
      }
      const resultArray = batchEncoder.decode(decodedResult);
      const number = resultArray[0];

      res.status(200).json({ number });
    } catch (error) {
      console.error(error);
      res.status(500).json({ error: 'Internal Server Error' });
    }
  } else {
    res.setHeader('Allow', ['POST']);
    res.status(405).end(`Method ${req.method} Not Allowed`);
  }
}

挙動確認

ファイルの作成などができたら実際にプロジェクトを立ち上げて挙動を確認していきましょう!

1. プロジェクトの立ち上げ

npm run dev

2. 2つの値を暗号化

number1number2には好きな値を入力してください。下記の例では105を暗号化しています。

curl -X POST http://localhost:3000/api/encrypt -H "Content-Type: application/json" -d '{"number1":"10", "number2":"5"}'

大量のアウトプットが出力されたと思います。実際の暗号化されたデータはdata/の中のcipherText1.txtcipherText2.txtで確認できます。

3. 演算

実際に演算してみましょう。今回は乗算をしてみたいと思います。下記のようにevaluate.tsの乗算の部分のみコメントを外します。

evaluate.ts
// 加算
// const accumulatedCipherText = seal.CipherText();
// evaluator.add(cipherTextLoad_1, cipherTextLoad_2, accumulatedCipherText);

// 乗算
const accumulatedCipherText = seal.CipherText();
evaluator.multiply(cipherTextLoad_1, cipherTextLoad_2, accumulatedCipherText);

// 減算
// const accumulatedCipherText = seal.CipherText();
// evaluator.sub(cipherTextLoad_1, cipherTextLoad_2, accumulatedCipherText);

// 累乗
// const accumulatedCipherText = seal.CipherText();
// evaluator.square(cipherTextLoad_1, cipherTextLoad_2, accumulatedCipherText);

そして下記コマンドで実行します。引数はいりません。上記手順で暗号化したファイルを読みにいっています。また演算の結果は data/accumulatedCipherText.txtとして保存されます。

curl -X POST http://localhost:3000/api/evaluate -H "Content-Type: application/json"

復号化で確認

それでは復号化して値を確認してみましょう。今回も演算した結果のファイルaccumulatedCipherText.txtを勝手に読みにいっているので引数は必要ありません。

curl -X POST http://localhost:3000/api/decrypt -H "Content-Type: application/json"

私の環境では105を乗算したので下記のような結果が返ってきました。ただしく50が返ってきて演算と復号化ができたことがわかります。。

まとめ

今回は特に暗号の説明はせずにとにかく実際に手元で準同型暗号を試すという意図で記事を書きました。このnode-sealでは他にも様々な計算をサポートしているので是非遊んでみてください。サポートしている演算の一覧が載っているgithubディレクトリを記しておきます。

https://github.com/s0l0ist/node-seal/blob/main/src/implementation/evaluator.ts

Discussion