カテゴリー
Nodejs/Crypto APIを使って簡単な認証機能付きWEBページを構築する
※ 当ページには【広告/PR】を含む場合があります。
2024/10/28

昨今では「Auth0」などに代表されるような高度なAPI認証サービスを利用する機会も多く、Webサイトの認証機能を自作することも随分減ったように感じます。
ただ、運営者用の連絡掲示板のようなちょっとしたお仕事用のページを作りたい場合など、小規模で限定的な用途であればさほど労力と費用をかけないで自前の認証機能を構築したいときがあります。
認証機能の多くはサーバーサイドで処理されるため、自作の認証機能づくりに不可欠なライブラリが
もう一つ、Nodejs cryptoと混同しやすいJavascriptライブラリに
こちらは各ブラウザが機能を提供しているクライアントサイドの暗号化関連のAPIライブラリとなります。
Nodejs cryptoとWeb cryptoの違いは、暗号化の操作をサーバーサイドでやるか、フロントエンドでやるか、という設計思想の違いであり、実装したい機能や暗号化の結果にほとんど違いはありません。
ただし、Nodejsのほうが、Bufferによる生バイト操作や、ファイルIOなど、実装の自由度が高いため、この記事内ではおおよそNodejs cryptoのほうでサンプルコードを多く紹介します。
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');
のようにハッシュ化した場合、一見良さそうにも思いますが、実際にはセキュリティ面ではあまり推奨できるやり方ではないようです。
そこで、いうなれば「署名付きのハッシュ関数」である
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
ポイントとなる
update
第一引数:
暗号化の対象となる文字列
第二引数:
入力エンコード。
=> utf8/ascii/binary から選択
第三引数:
出力エンコード。
=> binary/base64/hex
最終的に、
update
final
ソルト
ソルトを利用しない場合、同一のパスワードからは同一のハッシュ値が生成されることになり、安全性が低くなります。
他方、ソルトを利用することで、パスワードは同じでも、異なるハッシュ値が生成できるため、仮にハッシュ化したパスワードが流出しても、元のパスワードを特定ことはより困難になります。
ソルトはデータサイズは自由に決めることができますが、通常16バイト以上とするのが良いとされています。
なお、サルト自体の値は第三者に知られてしまっても、すぐに変更すれば問題はありません。
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
復号化を利用する側の一例を示すと以下のようになります。
なお、暗号化には先程説明した
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で認証機能を自作するためのCryptoの基本的な使い方をザッとダイジェストで説明していきました。
暗号化独特のパラメーターや考え方を抑えておくことで、既存のオンライン認証サービスの理解も進むため、実際に自分の指を動かしながら暗号化の技術を学習されるのも良いのでと思います。
記事を書いた人
ナンデモ系エンジニア
主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。
カテゴリー