やあやあ、wsn0672だよ
前回の記事で、ScratchのTurboWarp用クラウド変数サーバーをラズパイで建てたんだけど、
(まだの人は読んでね↓)
これだとセキュリティ皆無な状態なんですよね..
リンクさえ知っていれば誰でもアクセスして、クラウド変数のデータを盗み見したり書き換えたりできちゃいます
それを防ぐために今回はこのサーバーにトークン認証を実装していきます
そうすればトークンを知ってる人しかアクセスできないようになるというわけ
トークン認証の仕組み
トークン認証をつけていくわけなんだけど
クライアント側が
wss://your.cloud.server/cloud/?token=abcd1234
みたいにトークン情報を載せてアクセスしてくれればサーバー側でtoken=abcd1234
の部分を確認してトークンが合ってるかで認証できますね
まずはこのトークンを固定してやってみましょうか
固定トークン
例えばトークンをSuperSecretToken1234
にするとします
するとサーバー側は wss://your.cloud.server/cloud/?token=SuperSecretToken1234
に来たアクセスだけ通せば認証ができますね
フロント側の処理
まずはTurboWarpでパッケージ化したプロジェクトがトークン情報を載せてサーバーにアクセスするためにHTMLを改造していきましょう
パッケージ化したコードをVSCodeかなんかで開いて自分のクラウドサーバーのドメインを検索してください
するとこんなコードが見つかるはずです
try {
scaffolding.addCloudProvider(new
Scaffolding.Cloud.WebSocketProvider(["wss://your.server.domain/cloud/"], "room-name"));
} catch (error) {
console.error(error);
}
これの
["wss://your.server.domain/cloud/"]
の部分を
["wss://your.server.domain/cloud/?token=SuperSecretToken1234"]
と変更して、トークン情報を追加してやるだけです
これだけでフロント側の処理はOKです
それではサーバー側でこれを確認する処理を追加していきましょう
サーバー側の処理
まずはクラウド変数サーバーのファイルに移動してください
cd /path/to/server/file
僕の場合は/home/admin/cloud-server
でした
そしたらsrc/server.js
を弄ります
nano src/server.js
その中の
wss.on('connection', (socket, request) => {
という行を見つけてください
ここがws通信を開始するコードなのでここに認証のコードを入れていきます
const SECRET_TOKEN = "ここにトークンを貼る";
wss.on('connection', (socket, request) => {
const url = new URL(request.url, `ws://${request.headers.host}`);
const token = url.searchParams.get('token');
if (token !== SECRET_TOKEN) {
console.log('不正アクセス:', request.socket.remoteAddress);
socket.close();
return;
}
console.log('認証成功:', request.socket.remoteAddress);
// 既存の接続処理はこの下に
});
これを追加しましょう
そしたら保存して再起動
sudo systemctl restart turbowarp-cloud.service
これだけで固定トークンによる認証はOKです
トークン処理を入れたプロジェクトで動作確認してみましょう
クラウド変数がきちんと動作すればOKです
大丈夫そうだったら、試しにトークン処理が入ってないプロジェクトでアクセスしてみてください
クラウド変数が動かなければ正常です
(ここでコンソールを見てみると不正なクライアントが必死に再接続を試み続けてる面白い状態を見ることができますよ)
さて、固定トークンでの認証が成功しましたが、
これだとURLがバレたら意味なくない..?
と勘のいいガキは気が付いたのではないでしょうか
ですよね、これだとトークンが固定なので、トークンがバレたら終わりなわけです
だったら、トークンを時間と秘密鍵をもとに生成すれば、仮にトークンがバレても一定時間経てば無効になるので安全なのではないでしょうか
(もちろん秘密鍵とトークン生成処理がバレたら終わりだけど)
可変トークン
固定トークンとは違い、トークンが時間ベースで変化することで漏洩リスクを最小限にできます
実質的にワンタイムパスワード方式で認証を強化する仕組みです
時間と鍵をもとにトークンを生成するのはTOTPと同じ方式ですね
超簡単にやると、これだけで実現できます
function generateToken(secret) {
const time = Math.floor(Date.now() / 60000); // 60秒ごとに新トークン
return sha256(secret + time);
}
でもこれだとsecret + time
...構造が単純すぎですよね
さらにトークン切り替えの境目で一瞬認証エラーになるリスクもあります
(オンラインゲームによく使われるクラウド変数にはあってはいけない欠点です...)
なので
① secret + time ではなく標準的なTOTPでも使われているHMAC-SHA256という鍵付きハッシュ方式を採用
② クライアントが今のトークンを生成してもサーバー側は直前のトークンも許可する
↪︎ラグやズレは完全に吸収できる
これでお悩み解決です
フロント側を作っていきましょう
フロント側の処理
まずはHMAC-SHA256でトークンを生成する関数を追加しましょう
async function generateTOTP(secretKey) {
const encoder = new TextEncoder();
const keyData = encoder.encode(secretKey);
const timeData = new Uint8Array(new TextEncoder().encode(Math.floor(Date.now() / 60000).toString()));
const cryptoKey = await crypto.subtle.importKey(
'raw',
keyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signature = await crypto.subtle.sign('HMAC', cryptoKey, timeData);
const base64 = btoa(String.fromCharCode(...new Uint8Array(signature)));
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); // URL-safe
}
そしてWebSocketProviderの部分 ( try { ~~ ) をこう変更
(async () => {
const secretKey = "ここに秘密鍵を貼る";
const token = await generateTOTP(secretKey);
try {
scaffolding.addCloudProvider(new Scaffolding.Cloud.WebSocketProvider(
"wss://www.wsn0672.org/cloud/?token=" + token,
"p4-@オンライン広場Ver.6.5.sb3"
));
} catch (error) {
console.error(error);
}
})();
秘密鍵は適当な文字列で大丈夫です
これで、60秒ごとにトークンをHMAC-SHA256で生成され、URLにはその瞬間だけ有効なトークンが付くことになります
したらサーバー側でも同じ計算をして、トークンを照合すればいいわけです
サーバー側の処理
さっきserver.jsを書き換えた時のところをもう一度書き換えます
wss.on('connection', (ws, req) => {
const SECRET_KEY = "ここにフロント側のと同じ秘密鍵を貼る";
function generateTOTP(secret, time) {
const crypto = require('crypto');
const timeStr = time.toString();
const timeBuffer = Buffer.from(timeStr, 'utf8'); // UTF-8バイト列化
const hmac = crypto.createHmac('sha256', secret);
hmac.update(timeBuffer);
return hmac.digest('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, ''); // URL-safe
}
const now = Math.floor(Date.now() / 60000); // 60秒ごとに生成
const validTokens = [
generateTOTP(SECRET_KEY, now), // 今のトークン
generateTOTP(SECRET_KEY, now - 1) // 1つ前(ズレ対策)
];
const url = new URL(req.url, `ws://${req.headers.host}`);
const token = url.searchParams.get('token');
if (!validTokens.includes(token)) {
logger.info('不正なトークンからの接続: ' + req.socket.remoteAddress);
ws.close(4001, 'Invalid token');
return;
}
logger.info('トークン認証成功: ' + req.socket.remoteAddress);
// 既存の接続処理はこの下に
変えれたら保存してサーバーを再起動
これでクライアントとサーバーで生成されるトークンが同じになり、認証が通るようになります
時刻をもとにトークンを生成することに成功しました!
これでトークンが漏れても120秒で無効になり、使えなくなります
もっとセキュリティを高めたいという方は60秒設定のところを30秒とかに縮めればOKです
(60秒で十分だと思いますけどね)
最後に
この状態だと秘密鍵とトークン生成処理がHTMLに直書きされてます
今回僕の場合はログインした人だけがアクセスできるページに書いているので大丈夫ですが、普通に一般公開していると悪意のある人はトークン生成の処理のところをそのまま自分のコードに実装してあなたのクラウド変数を好きなように弄ってきますのでご注意ください
クラウド変数サーバーをインターネット公開している以上、100%安全はあり得ません
秘密鍵や認証ロジックは一般公開しない場所に置くのがベストです
ではまた