2007 年 1 月 1 日 1 時 21 分

VNC 認証のセキュリティ処理


このアーカイブは同期化されません。 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 メッセージは返す必要がある。



Copyright (c) 1994-2007 Project Loafer. All rights reserved.