Nodejs/Crypto APIを使って簡単な認証機能付きWEBページを構築する


※ 当ページには【広告/PR】を含む場合があります。
2024/10/28
【nodejsアプリ開発】続・SvelteKitでポータブルなバイナリアプリを作れるか(今度はいい感じに成功)
蛸壺の技術ブログ|Nodejs/Crypto APIで作る簡単な認証機能を構築する

昨今では「Auth0」などに代表されるような高度なAPI認証サービスを利用する機会も多く、Webサイトの認証機能を自作することも随分減ったように感じます。

ただ、運営者用の連絡掲示板のようなちょっとしたお仕事用のページを作りたい場合など、小規模で限定的な用途であればさほど労力と費用をかけないで自前の認証機能を構築したいときがあります。

認証機能の多くはサーバーサイドで処理されるため、自作の認証機能づくりに不可欠なライブラリが
『Node.js Crypto API』です。

Crypto | Node.js documentation

もう一つ、Nodejs cryptoと混同しやすいJavascriptライブラリに
『Web Crypto API』があります。

Crypto | MDN web doc

こちらは各ブラウザが機能を提供しているクライアントサイドの暗号化関連のAPIライブラリとなります。

Nodejs cryptoとWeb cryptoの違いは、暗号化の操作をサーバーサイドでやるか、フロントエンドでやるか、という設計思想の違いであり、実装したい機能や暗号化の結果にほとんど違いはありません。

ただし、Nodejsのほうが、Bufferによる生バイト操作や、ファイルIOなど、実装の自由度が高いため、この記事内ではおおよそNodejs cryptoのほうでサンプルコードを多く紹介します。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート】nodejsをこれから学びたい人のためのオススメ書籍&教材特集

Node.js Cryptoを使った暗号操作

まずNode.jsでのハッシュ化、暗号化・復号化の基本的な使い方を説明していきましょう。

ハッシュ化

「ハッシュ化」は後々"復号化"する必要がない場合に用いる暗号化の一種です。

単にハッシュというと、ある入力文字列をハッシュ関数へ与えて、ハッシュ値を出力する、というだけのものになります。

            
            import { createHash } from 'node:crypto';

const txt = 'hogehoge';
const hashedText = createHash('sha256').update(txt).digest('base64');

//👇Base64形式のハッシュ値が表示される
console.log(hashedText);
        
ハッシュ値を得る利点は、逆方向への計算が難しく、ハッシュ元の文字列を推測しにくくなるということにあります。

当然、同じ文字列、同じアルゴリズムでハッシュ値を出力した場合は、全く同じ結果を得ることになります。

秘密鍵ようなセキュリティ性の高い情報を含むハッシュ値を得たい場合、

            
            const secret_key = 'とっても秘密';
const hashedText = createHash('sha256').update(`絶対バレたくない鍵は${secret_key}`).digest('base64');
        
のようにハッシュ化した場合、一見良さそうにも思いますが、実際にはセキュリティ面ではあまり推奨できるやり方ではないようです。

そこで、いうなれば「署名付きのハッシュ関数」である
「HMAC」と呼ばれるハッシュ関数を利用することになります。

            
            import { createHmac } from 'node:crypto';

const secret_key = 'とっても秘密';
const hashedText = createHmac('sha256', secret_key).update(`絶対バレたくない言葉`).digest('base64');
        
とすることで、暗号化したいメッセージとハッシュ値を生成する秘密鍵を分離できて、より堅牢でセキュアなハッシュ値が得られるようです。

暗号化

先程のハッシュ化と異なり、暗号化した後のデータを、復号化できるもので、一般的にこちらを
「暗号化」を呼んでいることがほとんどです。

暗号化・復号化の可逆的な操作を行わせるため、一方方向の処理で済むハッシュ化よりもより複雑な仕組みを必要とします。

例えば、サーバー側で暗号化の処理を行うコードの一例としては以下のようになります。

            
            import { randomBytes, createCipheriv, scryptSync } from 'node:crypto';

//☆👇暗号化するアルゴリズム
const algorithm = "aes-256-ctr";
//☆👇秘密鍵となるパスワード
const secretKey = process.env.SERVER_SECRET_KEY;

const encrypt = (txt: string): Buffer => {
    //☆👇サルト
    const salt = randomBytes(16);
    const key = scryptSync(secretKey, salt, 32);

    //☆👇IV(初期化ベクトル)
    const iv = randomBytes(16);
    const cipher = createCipheriv(algorithm, key, iv);

    const encrypted = Buffer.concat([salt, iv, cipher.update(txt, "utf8"), cipher.final()]);

    return encrypted;
};
        
ここではいくつか、あまり聞き慣れない暗号化・復号化に必要なパラメーターとしてソルトIVが登場しているのが分かると思います。

暗号化処理はこの
ソルト(でハッシュ化したパスワード)IVを基に、createCipheriv関数でCipherivのクラスインスタンスを作成します。

作成した
Cipherivインスタンスからupdate関数を呼び出すことで、暗号化した結果を得ます。

また、
update関数は1回だけでなく何度か繰り返すことでより暗号化の手続きを複雑化することが可能です。

ポイントとなる
update関数の引数に関しては以下の通りです。

            
            第一引数:
    暗号化の対象となる文字列
第二引数:
    入力エンコード。
    => utf8/ascii/binary から選択
第三引数:
    出力エンコード。
    => binary/base64/hex
        
最終的に、update関数で暗号したデータと、final関数で得たチェック用終端データを繋げたデータを返すことで、後で復号化できるデータすることができます。

ソルト

「ソルト(Salt)」は、パスワードをハッシュ化する際に利用される、特定のバイト長のランダムな値です。

ソルトを利用しない場合、同一のパスワードからは同一のハッシュ値が生成されることになり、安全性が低くなります。

他方、ソルトを利用することで、パスワードは同じでも、異なるハッシュ値が生成できるため、仮にハッシュ化したパスワードが流出しても、元のパスワードを特定ことはより困難になります。

ソルトはデータサイズは自由に決めることができますが、通常16バイト以上とするのが良いとされています。

なお、サルト自体の値は第三者に知られてしまっても、すぐに変更すれば問題はありません。

IV(初期化ベクトル)

「IV(初期化ベクトル)」も、サルト同様に暗号化するために利用される決まったデータサイズをもつランダムな値です。

こちらはサルトによって生成した同じ鍵を使用した際にも、それを用いて複数暗号化する場合に、異なるIVを使うことで、暗号化時に最初のブロックを替えて、同じ結果の暗号データとならないようにすることが可能になります。

IVのデータサイズは最初のブロックサイズによって異なり、暗号化アルゴリズムによって決まる値となります。

IVの値を変えずに、何回も暗号化をした場合、データを比較したときに、ハッシュ化された鍵部分のデータも推測されやすくなるため、暗号化一回ごとにIVを新しく生成する必要があるとされています。

なお、IVの値自体は第三者に知られても、特に問題はありません。

復号化

暗号化と対になる機能が
「復号化」で、可逆暗号で運用したいなら必ずセットで実装しなければならない機能です。

復号化するロジックメソッドもサーバー側で準備しておきます。

            
            import { createDecipheriv, scryptSync } from 'node:crypto';

//☆👇暗号化するアルゴリズム
const algorithm = "aes-256-ctr";

//☆👇秘密鍵となるパスワード
const secretKey = process.env.SERVER_SECRET_KEY;

const decrypt = (ciphertext: Buffer, salt: Buffer, iv: Buffer) => {
    const key = scryptSync(secretKey, salt, 32);

    //👇createDecipheriv関数では暗号化したときに利用した暗号化アルゴリズム、鍵、IVを指定
    const decipher = createDecipheriv(algorithm, key, iv);
    const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);

    return decrypted.toString("utf8");
};
        
この復号化のメソッドを使って、クライアント側からリクエストされる暗号化された文字列(Base64やHEX等)を以下のような処理で復号化し、元の文字列を抽出するなどします。

このとき、
update関数を繰り返しの回数は、暗号化の時と同回数にし、最後に取得した終端データをfinal関数で取得したものと合わせてたBufferが、復号されたデータになります。

復号化を利用する側の一例を示すと以下のようになります。

なお、暗号化には先程説明した
encryptメソッドを使った場合です。

            
            //👇クライアントから送信されてきた文字列(ここではBase64形式と想定)
const et: string = '暗号化されたメッセージ';

//👇Base64をBufferへ変換
const ret = Buffer.from(et, 'base64');

//👇Bufferの先頭1〜16Bytes目がサルト
const salt = ret.subarray(0, 16);

//👇Bufferの17〜32Bytes目がIV
const iv = ret.subarray(16, 32);

//👇Bufferの33Bytes目以降から暗号化したデータ
const val = ret.subarray(32);

//👇データをUTF-8で復号化したものを得る
const decripted = decrypt(val, salt, iv);
        
復号化の処理は、見ての通り、暗号化した際の手順の逆工程を正しく反映させなければならないため、暗号化と復号化のロジックの実装は表裏一体となります。

Bufferの使い方

余談ですが、ちょっと古めのNodejs/cryptoで技術記事でBufferを得たいときに頻出するnew Buffer()というやり方で、

            
            const _buf = new Buffer('HOGE!');
        
というものですが、セキュリティホールとなることが指摘されて、現在のNodejsでは非推奨扱いとなっています。

この場合、代替として追加されたAPIが
Buffer.fromと、Buffer.alloc/Buffer.allocUnsafeを使うことになります。

            
            ///size(バイト数)指定
new Buffer(size)
//👇
Buffer.alloc(size)
//もしくはBuffer.allocUnsafe(...)

///size以外のString・Array・ArrayBuffer・Buffer型
new Buffer(string)
//👇
Buffer.from(string)
        

合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート】nodejsをこれから学びたい人のためのオススメ書籍&教材特集

まとめ

今回は、Nodejsで認証機能を自作するためのCryptoの基本的な使い方をザッとダイジェストで説明していきました。

暗号化独特のパラメーターや考え方を抑えておくことで、既存のオンライン認証サービスの理解も進むため、実際に自分の指を動かしながら暗号化の技術を学習されるのも良いのでと思います。
記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。

合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート】nodejsをこれから学びたい人のためのオススメ書籍&教材特集