2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

認証認可Advent Calendar 2024

Day 13

【TypeScript】SRP 認証を行える npm パッケージを実装する(後編)

Posted at

はじめに

こんにちは、梅雨です。

この記事は以下の記事の続きとなります。

前半ではサインアップの実装まで行いました。今回は実際にログインして認証を行えるようにするところまで実装してしていきたいと思います。

実装を行う

まずは、ログイン機能を実装していきます。

ログインの実装

クライアント用ログイン関数(1)

クライアントの signup() 関数では、2段階に分けてサーバにリクエストを送信します。

1段階目では鍵のペアを作成し、ユーザ名と公開鍵をサーバに送信します。

A=g^a\bmod N
packages/srp/src/client/signin.ts
import BigInteger from "big-integer";
import { g, N } from "../utils/constants";

type SignInInput = {
  userName: string;
  password: string;
};

export const signIn = async (
  url: string,
  { userName, password }: SignInInput
) => {
  // ランダムな a の値を生成
  const array = new Uint8Array(128);
  window.crypto.getRandomValues(array);
  const randomHex = Array.from(array)
    .map((byte) => byte.toString(16).padStart(2, "0"))
    .join("");
  const aValue = BigInteger(randomHex, 16);

  // A = g^a mod N
  const largeAValue = g.modPow(aValue, N);

  // userName, A をサーバに送信
  const res = await fetch(url, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      type: "USER_SRP_AUTH",
      authParameters: {
        USERNAME: userName,
        SRP_A: largeAValue.toString(16),
      },
    }),
  });
};

サーバ用ログイン関数(1)

サーバ側も同様に鍵のペアを作成します。ここで、サーバ側はブラウザ環境ではなく Node.js であるためランダム時の生成には crypto を使用します。crypto は Node.js の標準モジュールであるためインストールは不要ですが、Node.js の型定義はインストールしておく必要があります。

$ npm i -D @types/node
packages/srp/src/client/signin.ts
import BigInteger from "big-integer";
import { g, N } from "../utils/constants";
import { getRandomValues } from "crypto";

type SignInInput = { type: "USER_SRP_AUTH" };

export function signIn({ type }: { type: "USER_SRP_AUTH" }): {
  bValue: string;
  SRP_B: string;
};
export function signIn({ type }: SignInInput) {
  switch (type) {
    case "USER_SRP_AUTH":
      // ランダムな b の値を生成
      const array = new Uint8Array(128);
      getRandomValues(array);
      const randomHex = Array.from(array)
        .map((byte) => byte.toString(16).padStart(2, "0"))
        .join("");
      const bValue = BigInteger(randomHex, 16);

      // B = g^b mod N
      const largeBValue = g.modPow(bValue, N);

      return {
        bValue: bValue.toString(16),
        SRP_B: largeBValue.toString(16),
      };
    default:
      return;
  }
}

バックエンド API のエンドポイント実装(1)

エンドポイントでは、リクエストボディに指定されたユーザ名を使ってデータベースからソルトを取得します。そして、SALTSRP_BUSERNAME を返却します。

server/src/index.ts
app.post("/signin", (req, res) => {
  const { type } = req.body;

  switch (type) {
    case "USER_SRP_AUTH":
      const { authParameters } = req.body;
      const { USERNAME, SRP_A } = authParameters;

      db.get<{ id: number; name: string; salt: string; verifier: string }>(
        "SELECT * FROM users WHERE name = ?",
        USERNAME,
        (err, { name, salt }) => {
          const { SRP_B } = signIn({ type: "USER_SRP_AUTH" });
          res.send({
            SALT: salt,
            SRP_B,
            USERNAME: name,
          });
        }
      );
  }
});

ここで、SRP_B は公開鍵ですが、SRP_B とペアである秘密鍵(bValue)をサーバ側で保存する必要があります。

今回は express-session を用いて秘密鍵を保存しようと思います。

credential をクライアントから受け取る必要があるため、CORS の設定で credentials: true とすることを忘れないようにしましょう。

$ npm i express-session
$ npm i -D @types/express-session
import express from "express";
import cors from "cors";
import sqlite from "sqlite3";
import { signIn } from "srp/server";
+ import session from "express-session";

const app = express();

app.use(
  cors({
    origin: "http://localhost:3000",
+     credentials: true,
  })
);
app.use(express.json());
+ app.use(
+   session({
+     secret: "SECRET_KEY",
+     cookie: {
+       maxAge: 60000,
+     },
+   })
+ );
+
+ declare module "express-session" {
+   interface SessionData {
+     bValue: string;
+   }
+ }

// 略

app.post("/signin", (req, res) => {
  const { type } = req.body;

  switch (type) {
    case "USER_SRP_AUTH":
      const { authParameters } = req.body;
      const { USERNAME, SRP_A } = authParameters;

      db.get<{ id: number; name: string; salt: string; verifier: string }>(
        "SELECT * FROM users WHERE name = ?",
        USERNAME,
        (err, { name, salt }) => {
          const { SRP_B, bValue } = signIn({ type: "USER_SRP_AUTH" });
+           req.session.bValue = bValue;
+           req.session.save();
          res.send({
            SALT: salt,
            SRP_B,
            USERNAME: name,
          });
        }
      );
  }
});

続いて、バックエンドでは SRP_ASRP_B から共有鍵 u を計算する必要があります。この共有鍵 u もセッションに保存しておきましょう。

packages/srp/src/server/signin.ts
import BigInteger from "big-integer";
import { g, N } from "../utils/constants";
import { getRandomValues } from "crypto";
import hexHash from "../utils/hexHash";

type SignInInput = { type: "USER_SRP_AUTH"; SRP_A?: string } & {
  type: "USER_SRP_AUTH";
  SRP_A: string;
};

export async function signIn({
  type,
  SRP_A,
}: {
  type: "USER_SRP_AUTH";
  SRP_A: string;
}): Promise<{
  bValue: string;
  SRP_B: string;
  uValue: string;
}>;
export async function signIn({ type, SRP_A }: SignInInput) {
  switch (type) {
    case "USER_SRP_AUTH":
      const array = new Uint8Array(128);
      getRandomValues(array);
      const randomHex = Array.from(array)
        .map((byte) => byte.toString(16).padStart(2, "0"))
        .join("");
      const bValue = BigInteger(randomHex, 16);

      const largeBValue = g.modPow(bValue, N);

      // SRP_A と SRP_B の値から共有鍵 u を計算
      const uValue = await hexHash(SRP_A + largeBValue.toString(16));

      return {
        bValue: bValue.toString(16),
        SRP_B: largeBValue.toString(16),
        uValue,
      };
    default:
      return;
  }
}
server/src/index.ts
declare module "express-session" {
  interface SessionData {
    bValue: string;
+     uValue: string;
  }
}

// 略

app.post("/signin", (req, res) => {
  const { type } = req.body;

  switch (type) {
    case "USER_SRP_AUTH":
      const { authParameters } = req.body;
      const { USERNAME, SRP_A } = authParameters;

      db.get<{ id: number; name: string; salt: string; verifier: string }>(
        "SELECT * FROM users WHERE name = ?",
        USERNAME,
-         (err, { name, salt }) => {
-           const { SRP_B, bValue, uValue } = signIn({ type: "USER_SRP_AUTH" });
+         async (err, { name, salt }) => {
+           const { SRP_B, bValue, uValue } = await signIn({
+             type: "USER_SRP_AUTH",
+             SRP_A,
+           });
          req.session.bValue = bValue;
+           req.session.uValue = uValue;
          req.session.save();
          res.send({
            SALT: salt,
            SRP_B,
            USERNAME: name,
          });
        }
      );
  }
});

クライアント用ログイン関数(2)

クライアントも同様にしてサーバから受け取ったSRP_BSALTUSERNAME を用いて共有鍵 u を生成します。

また、ユーザー名、パスワード、共有鍵(u)、ソルトからセッションキー(K)を生成します。

S = (B - kg^x)^{a + ux}\mod n
K = H(S)

サーバにはユーザ名とセッションキーを送信しましょう。

import BigInteger from "big-integer";
import { g, N } from "../utils/constants";
import hexHash from "../utils/hexHash";

type SignInInput = {
  userName: string;
  password: string;
};

export const signIn = async (
  url: string,
  { userName, password }: SignInInput
) => {
  const array = new Uint8Array(128);
  window.crypto.getRandomValues(array);
  const randomHex = Array.from(array)
    .map((byte) => byte.toString(16).padStart(2, "0"))
    .join("");
  const aValue = BigInteger(randomHex, 16);

  const largeAValue = g.modPow(aValue, N);

  const res = await fetch(url, {
    method: "POST",
    credentials: "include",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      type: "USER_SRP_AUTH",
      authParameters: {
        USERNAME: userName,
        SRP_A: largeAValue.toString(16),
      },
    }),
  });

+   const { SALT, SRP_B, USERNAME } = await res.json();
+ 
+   const uValue = BigInteger(
+     await hexHash(largeAValue.toString(16) + SRP_B),
+     16
+   );
+ 
+   const usernamePassword = `${USERNAME}:${password}`;
+   const usernamePasswordHash = await hexHash(usernamePassword);
+   const xValue = BigInteger(await hexHash(SALT + usernamePasswordHash), 16);
+ 
+   const largeBValue = BigInteger(SRP_B, 16);
+   const kValue = BigInteger(await hexHash(N.toString(16) + g.toString(16)), 16);
+   const S = largeBValue
+     .subtract(kValue.multiply(g.modPow(xValue, N)))
+     .add(N)
+     .modPow(aValue.add(uValue.multiply(xValue)), N);
+   const K = BigInteger(await hexHash(S.toString(16)));
+
+   await fetch(url, {
+     method: "POST",
+     credentials: "include",
+     headers: {
+       "Content-Type": "application/json",
+     },
+     body: JSON.stringify({
+       type: "PASSWORD_VERIFIER",
+       authParameters: {
+         USERNAME,
+         SESSION_KEY: K.toString(16),
+       },
+     }),
+   });
};

サーバ用ログイン関数(2)

サーバは、検証子(v)、ソルト、共有鍵(u)、秘密鍵(b)からセッションキーを生成します。

また、型解析のために一部の実装を変更しました。

packages/srp/src/server/signin.ts
import BigInteger from "big-integer";
import { g, N } from "../utils/constants";
import { getRandomValues } from "crypto";
import hexHash from "../utils/hexHash";

type SignInInput = {
  type: "USER_SRP_AUTH" | "PASSWORD_VERIFIER";
  SRP_A?: string;
  SRP_B?: string;
  salt?: string;
  verifier: string;
  bValue?: string;
} & (
  | {
      type: "USER_SRP_AUTH";
      SRP_A?: undefined;
      SRP_B?: undefined;
      salt?: undefined;
      verifier: string;
      bValue?: undefined;
    }
  | {
      type: "PASSWORD_VERIFIER";
      SRP_A: string;
      SRP_B: string;
      salt: string;
      verifier: string;
      bValue: string;
    }
);

export async function signIn({
  type,
  verifier,
}: {
  type: "USER_SRP_AUTH";
  SRP_A?: undefined;
  salt?: undefined;
  verifier: string;
  bValue?: undefined;
  uValue?: undefined;
}): Promise<{
  bValue: string;
  SRP_B: string;
}>;
export async function signIn({
  type,
  SRP_A,
  SRP_B,
  salt,
  verifier,
  bValue,
}: {
  type: "PASSWORD_VERIFIER";
  SRP_A: string;
  SRP_B: string;
  salt: string;
  verifier: string;
  bValue: string;
}): Promise<{
  sessionKey: string;
}>;
export async function signIn({
  type,
  SRP_A,
  SRP_B,
  salt,
  verifier,
  bValue,
}: SignInInput) {
  switch (type) {
    case "USER_SRP_AUTH": {
      const array = new Uint8Array(128);
      getRandomValues(array);
      const randomHex = Array.from(array)
        .map((byte) => byte.toString(16).padStart(2, "0"))
        .join("");
      const bValue = BigInteger(randomHex, 16);

      const k = BigInteger(await hexHash(N.toString(16) + g.toString(16)), 16);

      const largeBValue = k
        .multiply(BigInteger(verifier, 16))
        .add(g.modPow(bValue, N))
        .mod(N);

      return {
        bValue: bValue.toString(16),
        SRP_B: largeBValue.toString(16),
      };
    }

    case "PASSWORD_VERIFIER": {
      const uValue = BigInteger(await hexHash(SRP_A + SRP_B), 16);

      const s = BigInteger(SRP_A, 16)
        .multiply(BigInteger(verifier, 16).modPow(uValue, N))
        .modPow(BigInteger(bValue, 16), N)
        .mod(N);

      const K = BigInteger(await hexHash(s.toString(16)), 16);

      return {
        sessionKey: K.toString(16),
      };
    }

    default:
      return;
  }
}

バックエンド API のエンドポイント実装(2)

エンドポイントではクライアントから送信されたセッションキーを受け取り、サーバ内部で計算されたセッションキーとの一致を比較します。

server/src/index.ts
app.post("/signin", (req, res) => {
  const { type } = req.body;

  if (type === "USER_SRP_AUTH") {
    const { authParameters } = req.body;
    const { USERNAME, SRP_A } = authParameters;

    db.get<{ id: number; name: string; salt: string; verifier: string }>(
      "SELECT * FROM users WHERE name = ?",
      USERNAME,
      async (err, { name, salt, verifier }) => {
        const { SRP_B, bValue } = await signIn({
          type: "USER_SRP_AUTH",
          verifier,
        });

        req.session.bValue = bValue;
        req.session.SRP_A = SRP_A;
        req.session.SRP_B = SRP_B;
        req.session.save();

        res.send({
          SALT: salt,
          SRP_B,
          USERNAME: name,
        });
      }
    );
  } else if (type === "PASSWORD_VERIFIER") {
    const { authParameters } = req.body;
    const { USERNAME, SESSION_KEY } = authParameters;

    db.get<{ id: number; name: string; salt: string; verifier: string }>(
      "SELECT * FROM users WHERE name = ?",
      USERNAME,
      async (err, { salt, verifier }) => {
        const { bValue, SRP_A, SRP_B } = req.session;

        const { sessionKey } = await signIn({
          type: "PASSWORD_VERIFIER",
          SRP_A: SRP_A!,
          SRP_B: SRP_B!,
          salt,
          verifier,
          bValue: bValue!,
        });

        req.session.destroy((err) => {
          err && console.log(err);
        });

        sessionKey === SESSION_KEY ? res.sendStatus(200) : res.sendStatus(401);
      }
    );
  }
});

これで SRP を行うパッケージの実装ができました。

おわりに

パッケージとしてまだまだ不十分なところが多いので、時間がある時に改善したいと思います。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?