このアーカイブは同期化されません。 mixi の日記が更新されても、このアーカイブには反映されません。
画面が出たので少し安心した。
ここで、以前保留にしていた VNC 認証をやろう。
VNC 認証はパスワードによる単純な認証である。
ユーザ名などはなく、パスワードのみなので、
無線 LAN のキーのように、利用者で共有されることになる。
VNC 認証の流れについては、セキュリティ処理の所で解説した。
http://mixi.jp/view_diary.pl?id=300480641&owner_id=2300658
では、実装してみよう。
========== RFBSession#processVNCAuthentication ==========
// VNC Authentication セキュリティ処理
private void processVNCAuthentication(RFBContext context,
RFBInputStream in, RFBOutputStream out) throws IOException {
// ランダムな 16 バイトチャレンジを生成
byte[] challenge = new byte[16];
{
SecureRandom r = new SecureRandom();
r.nextBytes(challenge);
}
// パスワード照合用のハッシュを作成
byte[] hash;
try {
// パスワードを用意
String password = "password"; // 最大 8 文字
// ビットの順番を入れ替えながら byte に格納
byte[] secret = new byte[8];
for (int i = 0; i < 8 && i < password.length(); ++i) {
// US-ASCII へ
int code = password.charAt(i);
// 下位 8 ビットを左右反転
code = (code >>> 4) & 0x0f | (code & 0x0f) << 4; // 45670123
code = (code >>> 2) & 0x33 | (code & 0x33) << 2; // 67452301
code = (code >>> 1) & 0x55 | (code & 0x55) << 1; // 76543210
secret[i] = (byte)code;
}
Cipher c = Cipher.getInstance("DES/ECB/NoPadding");
SecretKeySpec key = new SecretKeySpec(secret, "DES");
c.init(Cipher.ENCRYPT_MODE, key);
hash = c.doFinal(challenge);
} catch (Exception e) {
e.printStackTrace();
throw new IOException(e);
}
// チャレンジを送信
out.write(challenge);
out.flush();
// レスポンスを受信
byte[] response = new byte[16];
in.readFully(response);
// 比較
boolean succeeded = Arrays.equals(response, hash);
// SecurityResult で可否を返す
SecurityResultMessage result;
if (!succeeded) {
result = new SecurityResultMessage(
SecurityResultMessage.STATUS_FAILED,
"VNC Authentication: Invalid password.");
out.writeMessage(context, result);
out.flush();
throw new IOException(result.getErrorMessage());
}
result = new SecurityResultMessage(SecurityResultMessage.STATUS_OK);
out.writeMessage(context, result);
out.flush();
}
========== end of RFBSession#processVNCAuthentication ==========
かなり長めのコードになった。
実は、RFB の仕様書には VNC 認証について書いてあるが、
その内容は不十分であり、実装するのに手間が掛かった。
そこで、上記コードのポイントを書いておこう。
まず、チャレンジとして適当な 16 バイトを生成する。
Random でもよかったが、今回は SecureRandom を利用した。
次に、チャレンジをクライアントに送信する前に、
チャレンジをサーバが知っているパスワードで暗号化し、
その結果(パスワードハッシュ)を生成しておく。
この手順にはコツがあり、
まずパスワードを US-ASCII でバイト配列に直した後、
8 バイトになるように後ろに値 0 のバイトを追加する。
そして、各バイトのビットの順序を反転する。これが重要だ。
これが、DES 暗号化用のキーとなる。
DES では、暗号化キーの各バイト中の最下位 1 ビットを、
DES 自身がパリティとして利用することになっているため、
キーのバイトの上位 7 ビットしか意味を成さない。
なので、DES は 8 バイトのキーなのに、56 bit 長なのだ。
さて、VNC 認証ではパスワードをキーとするのだが、
パスワードは US-ASCII なので 7 ビットであり、
各バイトの最上位ビットは常に 0 である。
例えば、文字 A の場合、01000001 (0x41) となる。
しかしながら DES では最下位のバイトが無視されるため、
このままキーとして利用してしまうと、
文字 @、01000000 (0x40) が文字 A と同一視されてしまう。
これを防ぐためには、7 ビットを上位に移動する必要がある。
そこで、各バイトのビットの順序を反転するのだ。
そうすると、文字 A は、10000010 (0x82) となり、
DES のキーとして利用しても情報が失われない。
DES 暗号化用のキーが用意できたら、
いよいよチャレンジバイトを DES で暗号化する。
Java で暗号化を行うためには、
javax.crypto.Cipher クラスを利用する。
まず Cipher#getInstance で暗号オブジェクトを生成する。
プロバイダは DES だ。VNC 認証では、
初期化ベクタ等は使わず、独立して暗号化するので、
ブロック暗号は ECB を指定する。
そして、入力と確実に同じ長さを得るため、
NoPadding を指定して余計な詰め物を排除する。
続いて、javax.crypto.spec.SecretKeySpec を使い、
秘密鍵オブジェクトを生成する。
暗号オブジェクトと秘密鍵が用意できたら、
Cipher#init を呼び出して暗号化方向に初期化し、
Cipher#doFinal を呼び出してチャレンジを暗号化する。
その戻り値が、パスワードハッシュとなる。
ここまで用意できたら、クライアントにチャレンジを送り、
16 バイトのレスポンスをクライアントから受け取る。
クライアントは上記同様の処理を行っているはずなので、
ユーザがパスワードを知っていれば、
レスポンスがパスワードハッシュと一致するはずである。
サーバは、これらを比較して認証の可否を決め、
SecurityResult メッセージでクライアントに通知する。
VNC 認証の場合は、例え RFB 3.3 の場合でも
SecurityResult メッセージは返す必要がある。