💭

Node.jsとTypeScriptで 準同型暗号を扱う API の構築

2024/02/23に公開

はじめに

ETHGlobalハッカソンで準同型暗号とZKPを使用してスコアをオンチェーン上で送信し合うアプリケーションを開発しました。Circuit内でFHEの復号化をするのにどうしたかなど記事にしていこうと思います。
https://ethglobal.com/showcase/trusted-score-p7woy

準同型暗号(HE)は、暗号化されたデータ上で直接計算を可能にする技術です。この記事では、Node.js と TypeScript を使用して FHE に関連する基本的な API を構築する方法を紹介します。Paillier 暗号システムを例に、鍵生成、暗号化、(暗号化されたデータのままの)加算、復号化の各エンドポイントを実装します。
Circuit内でどのように復号化処理を行なったかについては次回の記事で記載します。

  • 参考github

https://github.com/Mameta29/paillier-crypto

環境設定

必要なツールとライブラリ

  • Node.js
  • TypeScript
  • ts-node
  • Express.js
  • paillier-bigint

セットアップ手順

  1. プロジェクト初期化
    新しいディレクトリを作成し、その中で以下のコマンドを実行して Node.js プロジェクトを初期化します。
npm init -y
  1. TypeScript のインストール
    TypeScript と ts-node(TypeScript を直接実行できるツール)をインストールします。
npm install --save-dev typescript ts-node
  1. Express.js と paillier-bigint のインストール
    Express.jsと paillier-bigint(Paillier 暗号システムを実装するライブラリ)をインストールします。
npm install express body-parser paillier-bigint
  1. TypeScript 設定ファイルの生成
    tsconfig.json ファイルを生成してプロジェクトで TypeScript を使用できるようにします。
npx tsc --init

API エンドポイントの実装

鍵生成、暗号化、加算そして復号化をエンドポイントを分けてそれぞれファイルを作成します。

  1. 鍵生成 (generateKeyPairs.ts)
generateKeyPairs.ts
import { Request, Response } from 'express';
import { promises as fsPromises } from 'fs';
import path from 'path';
import * as paillierBigint from 'paillier-bigint';

function bigintToJson(key: any): string {
    return JSON.stringify(key, (key, value) =>
        typeof value === 'bigint' ? value.toString() : value
    );
}

export const generateKeyPairs = async (req: Request, res: Response) => {
    try {
        const { name } = req.body;
        // 鍵長を指定
        const keyLength = Number(process.env.KEY_LENGTH) || 1024;
        console.log('Key Length:', keyLength);

        if (!name) {
            return res.status(400).json({ error: 'Bad Request. Please provide your name!' });
        }

        // Paillierキーペアを生成
        const { publicKey, privateKey } = await paillierBigint.generateRandomKeys(keyLength);

        // ファイルパスを定義
        const pubKeyPath = path.join(__dirname, 'data', `${name}-publicKey.json`);
        const priKeyPath = path.join(__dirname, 'data', `${name}-privateKey.json`);

        // キーをJSON形式でファイルに非同期で書き込む
        await fsPromises.writeFile(pubKeyPath, bigintToJson(publicKey), 'utf8');
        await fsPromises.writeFile(priKeyPath, bigintToJson(privateKey), 'utf8');

        res.status(200).json({
            message: "Keys were generated and saved successfully.",
        });
    } catch (error) {
        console.error(error);
        res.status(500).json({ error: 'Internal Server Error' });
    }
};
  1. 暗号化(encrypt.ts)
    指定された数値を Paillier 公開鍵を使用して暗号化します。
encrypt.ts
import { Request, Response } from 'express';
import { promises as fsPromises } from 'fs';
import path from 'path';
import * as paillierBigint from 'paillier-bigint';

export const encrypt = async (req: Request, res: Response) => {
    try {
        const { num, name } = req.body;
        console.log("encrypt body", req.body);
        const m = BigInt(num);

        const pubKeyPath = path.join(__dirname, 'data', `${name}-publicKey.json`);
        const publicKeyJson = await fsPromises.readFile(pubKeyPath, 'utf8');
        const publicKeyObj = JSON.parse(publicKeyJson);

        const publicKey = new paillierBigint.PublicKey(
            BigInt(publicKeyObj.n),
            BigInt(publicKeyObj.g)
        );

        const encrypted = publicKey.encrypt(m);

        res.status(200).json({ encrypted: encrypted.toString() });
    } catch (error) {
        console.error(error);
        res.status(500).json({ error: 'Internal Server Error' });
    }
};
  1. 加算(add.ts)
    二つの暗号化された数値を加算し、結果として暗号化された合計を返します。
add.ts
import { Request, Response } from 'express';
import { promises as fsPromises } from 'fs';
import path from 'path';
import * as paillierBigint from 'paillier-bigint';

export const add = async (req: Request, res: Response) => {
    try {
        const { encNum1, encNum2, name } = req.body;
        const c1 = BigInt(encNum1);
        const c2 = BigInt(encNum2);

        const pubKeyPath = path.join(__dirname, 'data', `${name}-publicKey.json`);
  
        const publicKeyJson = await fsPromises.readFile(pubKeyPath, 'utf8');
        const publicKeyObj = JSON.parse(publicKeyJson);

        const publicKey = new paillierBigint.PublicKey(
            BigInt(publicKeyObj.n),
            BigInt(publicKeyObj.g)
        );

        const encryptedSum = publicKey.addition(c1, c2);

        res.status(200).json({ encryptedSum: encryptedSum.toString() });
    } catch (error) {
        console.error(error);
        res.status(500).json({ error: 'Internal Server Error' });
    }
};
  1. 復号化(decrypt.ts)
    暗号化された数値を Paillier 秘密鍵を使用して復号化します。
decrypt.ts
import { promises as fsPromises } from 'fs';
import { Request, Response } from 'express';
import path from 'path';
import * as paillierBigint from 'paillier-bigint';

export const decrypt = async (req: Request, res: Response) => {
    try {
        const { encNum, name } = req.body;
        const encrypted = BigInt(encNum);

        const priKeyPath = path.join(__dirname, 'data', `${name}-privateKey.json`);
        const pubKeyPath = path.join(__dirname, 'data', `${name}-publicKey.json`);
  
        const privateKeyJson = await fsPromises.readFile(priKeyPath, 'utf8');
        const privateKeyObj = JSON.parse(privateKeyJson);

        const publicKeyJson = await fsPromises.readFile(pubKeyPath, 'utf8');
        const publicKeyObj = JSON.parse(publicKeyJson);

        const publicKey = new paillierBigint.PublicKey(
            BigInt(publicKeyObj.n),
            BigInt(publicKeyObj.g)
        );
        const privateKey = new paillierBigint.PrivateKey(
            BigInt(privateKeyObj.lambda),
            BigInt(privateKeyObj.mu),
            publicKey
        );

        const decrypted = privateKey.decrypt(encrypted);

        res.status(200).json({ decrypted: decrypted.toString() });
    } catch (error) {
        console.error(error);
        res.status(500).json({ error: 'Internal Server Error' });
    }
};

サーバーの設定

Express アプリケーションを server.ts に統合し、ts-node を使用してサーバーを起動します。

import express from 'express';
import bodyParser from 'body-parser';
import { add } from './add';
import { encrypt } from './encrypt';
import { generateKeyPairs } from './generateKeyPairs';
import { decrypt } from './decrypt';

const app = express();
const port = 3000;

app.use(bodyParser.json());

// エンドポイントをアプリケーションに登録
app.post('/api/add', add);
app.post('/api/encrypt', encrypt);
app.post('/api/generateKeyPairs', generateKeyPairs);
app.post('/api/decrypt', decrypt);

app.listen(port, () => {
    console.log(`Server listening at http://localhost:${port}`);
});

サーバー起動&テスト

実際にサーバーを起動して curl コマンドで暗号化や復号化を試します。
1.まずサーバーを起動します。
ts-nodeをインストールしているのでtypescriptのコンパイルなどは内部でやってくれています。

ts-node server.ts
  1. 鍵生成
    鍵を生成するには、generateKeyPairs エンドポイントに POST リクエストを送信します。nameは任意の名前をつけてください。以降の手順では、ここでつけたnameと同じものを使用してください。
curl -X POST http://localhost:3000/api/generateKeyPairs \
-H "Content-Type: application/json" \
-d '{"name": "mameta"}'

下記では、ユーザー "mameta" のための Paillier 公開鍵と秘密鍵を生成し、それらをサーバーのファイルシステムに保存します。

  1. 暗号化
    生成された公開鍵を使用して数値を暗号化します。
curl -X POST http://localhost:3000/api/encrypt \
-H "Content-Type: application/json" \
-d '{"num": "100", "name": "mameta"}'

下記では数値 100 を "mameta" の公開鍵で暗号化し、暗号化された数値を返します。

下記では数値 200 を "mameta" の公開鍵で暗号化し、暗号化された数値を返します。

  1. 加算
    二つの暗号化された数値を加算します。このリクエストは、二つの暗号化された数値の加算を行い、その結果を暗号化された形で返します。<暗号化された数値1> と <暗号化された数値2> は、暗号化ステップで暗号化した数値に置き換えてください。
curl -X POST http://localhost:3000/api/add \
-H "Content-Type: application/json" \
-d '{"encNum1": "<暗号化された数値1>", "encNum2": "<暗号化された数値2>", "name": "mameta"}'

下記では上記で暗号化した100と200を暗号化まま加算し、暗号化された300(のはず)の値を返します。

  1. 復号化
    暗号化された数値を復号化します。
curl -X POST http://localhost:3000/api/decrypt \
-H "Content-Type: application/json" \
-d '{"encNum": "<暗号化された数値>", "name": "mameta"}'

下記では上記で加算した結果を復号化した例です。きちんと"300"が返ってきていることがわかります。

まとめ

paillier-bigintライブラリを使用することで簡単に実装して試すことができます。Circuit内ではこのライブラリが使えないので、次回はライブラリを使用せずに復号化する方法を解説します。

Discussion