今回達成すること
今回は、前回実装したローカルストレージの有効期限を暗号化する処理を行います。
完成イメージ
注意点
今回実装する暗号化処理はすぐ解読されます。(理由は記事下部に記載)
実装しても、しなくてもセキュリティレベルに変わりはありません。
あくまで、有効期限を丸見えにしないための実装とお考えください。
有効期限を暗号化する
数字を隠し、有効期限を一目で理解できないようにするため、JavaScriptの暗号化モジュールcrypto-js(クリプト・ジェーエス)を使って、有効期限を暗号化します。
モジュールのインストール
まずはcrypto-js
をインストールしましょう。
root $ docker-compose run --rm front yarn add crypto-js
...
# 成功
success Saved 1 new dependency.
info Direct dependencies
└─ crypto-js@4.0.0
info All dependencies
└─ crypto-js@4.0.0
暗号と複合に使う鍵を生成する
Node.jsに入っているcrypto
モジュールを使ってランダムな文字列を生成します。
この文字列は、暗号化と複合化に使用する鍵となります。
root $ docker-compose run --rm front node
> crypto.randomBytes(20).toString('hex')
'e758af258be55eddd5ab5d28cee60edbf5b62e43'
-
crypto.randomBytes(<サイズ>)
... 擬似ランダムデータを生成する。サイズはバイト数を表し、20の場合40文字となります。
参考: JavaScriptでランダムな文字列/文字を生成する - StackOverflow
ちなみに、
crypto-js
をインストールせずとも、Node.jsのcrypto
モジュールを使って暗号化と複合化を実装することができます。 -
toString(<出力形式>)
... 指定した出力形式で文字列に変換する。hex
、base64
、binary
、utf8
などが指定できます。
コンソールから抜けましょう。
> 「control」+「D」
環境変数に鍵を設定する
「front」ディレクトリの
front/.env
APP_NAME=BizPnanner
// 追加
CRYPTO_KEY=e758af258be55eddd5ab5d28cee60edbf5b62e43
publicRuntimeConfig
にも登録しましょう。
export default {
...
// public ENV
// Doc: https://nuxtjs.org/guide/runtime-config/
publicRuntimeConfig: {
appName: process.env.APP_NAME,
// 追加
cryptoKey: process.env.CRYPTO_KEY
},
...
}
環境変数の読み込みを行うため、frontサービスを再起動します。
root $ docker-compose restart front
crypto-jsを読み込む
「plugins」ディレクトリのcrypto-js
を読み込みます。
front/plugins/auth.js
// 追加
// Doc: https://www.npmjs.com/package/crypto-js
const cryptoJs = require('crypto-js')
const storage = window.localStorage
const keys = { exp: 'exp' }
class Authentication {
...
}
暗号化メソッドを追加
有効期限を暗号化するメソッドをコンストラクタの下に追加します。
front/plugins/auth.js
...
class Authentication {
constructor (ctx) {
this.store = ctx.store
this.$axios = ctx.$axios
this.error = ctx.error
// 追加
this.$config = ctx.$config
}
// 追加
// 有効期限を暗号化
encrypt (exp) {
const expire = String(exp * 1000)
return cryptoJs.AES.encrypt(expire, this.$config.cryptoKey).toString()
}
...
}
// $configを追加
export default ({ store, $axios, error, $config }, inject) => {
inject('auth', new Authentication({ store, $axios, error, $config }))
}
-
String(exp * 1000)
... 数字は暗号化時にエラーとなるため、文字列に変換します。オブジェクトの場合は、
JSON.stringify(object)
を使用します。参考: crypto-js - npm
-
AES.encrypt('文字列', '鍵')
... 文字列を暗号化する。複合する時は、暗号化と同じ鍵を使用します。
複合化メソッドを追加
暗号化した有効期限を複合化するメソッドを追加します。
try catch
を使って、失敗した場合にストレージを削除します。
front/plugins/auth.js
...
class Authenticator {
...
encrypt (exp) {
...
}
// 追加
// 有効期限を複合化
decrypt (exp) {
try {
const bytes = cryptoJs.AES.decrypt(exp, this.$config.cryptoKey)
return bytes.toString(cryptoJs.enc.Utf8) || this.removeStorage()
} catch (e) {
return this.removeStorage()
}
}
...
}
...
-
AES.decrypt('暗号文字列', '鍵')
... 暗号化した文字列を複合したオブジェクトを返す。鍵は暗号化時の鍵と同じ鍵を使用します。
bytes.toString
で、オブジェクトを元の文字列に戻します。
有効期限を取得するメソッドを編集
前回追加した、ストレージから暗号化文字列を取得するgetExpire()
を編集します。
- ストレージに値が存在する場合は
decrypt(デクリプト)
を呼び、 - 存在しない場合は
null
を返します。
front/plugins/auth.js
...
class Authenticator {
...
removeStorage () {
...
}
// storageの有効期限を複合して返す
getExpire () {
// 追加
const expire = storage.getItem(keys.exp)
return expire ? this.decrypt(expire) : null
// 削除
// return storage.getItem(keys.exp)
}
...
}
...
有効期限を保存するメソッドを編集
前回追加したsetStorage
を編集します。
有効期限をencrypt()
で暗号化し、ストレージに保存します。
front/plugins/auth.js
...
class Authenticator {
...
decrypt (exp) {
...
}
// storageに保存
setStorage (exp) {
// 追加
storage.setItem(keys.exp, this.encrypt(exp))
// 削除
// storage.setItem(keys.exp, exp * 1000)
}
...
}
...
有効期限の暗号化を確認してみよう
コンテナを起動して、http://localhost:8080/login にアクセスしてください。
確認のため、getExpire()
メソッドを追加します。
front/pages/login.vue
<template>
<bef-login-form-card #form-card-content>
<!-- 追加 -->
{{ $auth.getExpire() }}
...
</bef-login-form-card>
</template>
有効なユーザーでログインしてみましょう。
ローカルストレージには暗号化した有効期限が保存され、 HTMLには複合した有効期限が表示されます。
改ざんしてみよう
ローカルストレージのexp
の値を適当に書き換えて、リロードしてみましょう。
ストレージは削除されて、HTMLには何も表示されなくなりました。
テストコードを削除する
確認が取れたら、追加したテストコードを削除してください。
front/pages/login.vue
<template>
<bef-login-form-card #form-card-content>
<!-- {{ $auth.getExpire() }} 削除 -->
...
</template>
Herokuに環境変数を登録する
忘れないうちに本番環境のHerokuにも、CRYPTO_KEY
を登録しておきましょう。
「front」ディレクトリ上でheroku config:set
を実行してください。
root $ cd front
front $ heroku config:set CRYPTO_KEY=<適当な値>
front $ heroku config
APP_NAME: BizPlanner
CRYPTO_KEY: b834...
だが、しかし
「やった!これでフロントエンドでも完璧な認証ができた」と思った方。すみません。
この有効期限は簡単に解読されます。。。
なぜか
これは、鍵に使用したCRYPTO_KEY
が漏洩しているからです。
publicRuntimeConfig
に登録した値は、window.__NUXT__.congfig
に登録され、HTMLに展開されます。
じゃあどうすれば
結論は、「どうもしなくて良い」です。
今回用意した鍵は、有効期限を隠すためだけに使用します。
漏洩した鍵を使って有効期限を偽造したところで、Cookieのトークンが正しくなければデータベースのユーザー情報にたどりつくことができません。
先で実装しますが、無効なトークンをリクエストした場合は即座に認証エラーを吐き出し、ログアウトさせます。
つまり、正しいトークンを持たないユーザーはログイン状態を維持できない設計となっています。
env
プロパティに登録した値もそうですが、基本的にクライアントで使用する環境変数はソースコード上に展開されます。
今回実装した有効期限の暗号化は、有効期限を隠すためのものであって、守るためのものではないことをご理解ください。
クライアントで重要な鍵を扱う必要がある場合は、サーバーにリクエストを送ってサーバー側で判定をする実装がベストです。
コミットしとく
今回の実装は異常です。
コミットしときましょう。
root $ cd front
front $ git commit -am "add_crypto-js_module" && cd ..
root $
まとめと予告
今回は、ローカルストレージに保存した有効期限をcrypto-js
を使って暗号化しました。
次回は、Vuexのユーザーを維持して、ログインしたままにする処理を実装します。
どうぞ、お楽しみに〜!
本番環境の環境変数について、まとめましたのでお時間ある方はご覧ください↓
(コラム)Nuxt.js SPA v2.13+ Herokuデプロイ時の環境変数の取り扱い
env プロパティに登録した環境変数
envプロパティは、クライアント側で使用できる環境変数を定義します。
nuxt.config.js
export default {
env: {
cryptoKey: process.env.CRYPTO_KEY
}
}
env
プロパティに登録した環境変数は、コンパイル中に変換され値がセットされます。
コンパイルとは、Nuxt.jsのコードを純粋なJavaScriptに変換する事です。
コンパイルが実行されるタイミングは、nuxt build
コマンドが実行された時です。
nuxt build
は、yarn run build
コマンドを叩いた時に実行されます。
Dockerを使ったHerokuデプロイを行なっている場合、RUN yarn run build
を行なっています。
つまり、build
コマンド以前に環境変数の値を定義していないと、Nuxt.jsのenv
プロパティで参照することができません。
結論
env
プロパティに定義する環境変数は、ENV
で定義する必要があります。
Dockerを使ったNuxt SPAモードでは、$ heroku config:set
で定義した環境変数を、env
プロパティで参照することができません。
env プロパティの環境変数は漏洩します
env
プロパティに登録された環境変数の値は、JavaScript内に展開され、誰でも参照できる値となるので取り扱いにはご注意ください。
JavaScriptに展開されたenvプロパティ(デベロッパーツールのSourcesタブより)
return e.context || (e.context = {
isStatic: !0,
isDev: !1,
isHMR: !1,
app: e,
store: e.store,
payload: n.payload,
error: n.error,
base: "/",
env: {
cryptoKey: "b834d7320be976..."
}
}
publicRuntimeConfig に登録した環境変数
ここに登録した環境変数は、クライアントとサーバーの両方から参照できます。クライアントで使用する環境変数を登録します。
nuxt.config.js
export default {
publicRuntimeConfig: {
cryptoKey: process.env.CRYPTO_KEY
}
}
結論
$ heroku config:set
で定義した環境変数をクライアントで使用したい場合は、ここに登録します。
publicRuntimeConfig の環境変数は漏洩します
publicRuntimeConfig
に登録された環境変数は、window.__NUXT__.config
に登録されます。
この値は、HTMLファイルの<script>
タグ内で展開されるので、誰でも参照できます。
HTMLに展開されたpublicRuntimeConfig
<script>
window.__NUXT__ = {
config: {
cryptoKey: "b834d7320be976..."
},
staticAssetsBase: void 0
}
</script>
privateRuntimeConfig に登録した環境変数
ここに登録した環境変数は、サーバーのみで参照できます。主にサーバーサイドレンダリング時に使用する環境変数を登録します。
publicRuntimeConfig
と同じ値がある場合、privateRuntimeConfig
の値が優先されます。
nuxt.config.js
export default {
privateRuntimeConfig: {
cryptoKey: process.env.CRYPTO_KEY
}
}
privateRuntimeConfig
に登録した環境変数は、クライアントからは参照することができません。
よって、ソースコードから環境変数が漏洩することはありません。
ただ、SPAモードでは、クライアントでレンダリングを行なっているため、ここに登録した値を参照することはできません。無念。
(おしまい。)