Lazy Diary @ Hatena Blog

PowerShell / Java / miscellaneous things about software development, Tips & Gochas. CC BY-SA 4.0/Apache License 2.0

TFTPでファイルのリクエストがタイムアウトになる

背景

  • バイスのブート時にイメージをTFTPで流しこみたい。
  • TFTPで通信する際、デバイスIPアドレスは192.168.11.1、TFTPサーバは192.168.11.168で固定になっている。
  • TFTPサーバにはWindows 10上のPumpKINを使っている。
  • PumpKINの設定で、リクエスト・レスポンスともudp/69を使うよう設定している。
  • TFTPサーバとデバイスはLANケーブルで直接接続している。

問題

  • バイスからTFTPのリクエストは来るが、以下のようにPumpKINのコンソールにエラーが出力され、正しくファイルが返送されない。
[10/07/20 22:20:35] 'firmware_XXXX.XXX' of type 'octet' is requested from 192.168.11.1
[10/07/20 22:20:35] UDP packet receive failed
[10/07/20 22:20:40] Transmission of 'firmware_XXXX.XXX' is timed out
[10/07/20 22:20:40] Transfer of 'firmware_XXXX.XXX' was aborted
  • Windows 10上で192.168.11.168に対してtftpでファイルをリクエスト(tftp -i 192.168.11.168 get firmware_XXXX.XXX)すると正常に取得できる。そのため、WindowsファイアウォールでPumpKINがブロックされているとか、TFTPの待ち受け用ポートは開放されているが返送用ポートが閉じているとか、PumpKINがファイルを参照できていない、といった問題ではない。
  • WireSharkでLANケーブルが挿さっているNICのパケットをキャプチャすると、TFTPのリクエストのパケットが192.168.11.1→192.168.11.168へ正常に飛んでいるのが確認できる。たが、その後192.168.11.168→192.168.11.1へ応答のパケットがひとつも出ていっていない。
  • エラーメッセージはタイムアウトした旨の内容になっている。しかし、WireSharkで確認するとリクエストで設定されているタイムアウトはデフォルトの5秒のまま。

原因

IPアドレス(192.168.11.1)の重複が原因。 Windows 10上にVMWareをセットアップしており、その際に作成されたVMware Network Adapter VMnet8(VMWare Virtual Ethernet Adapter)のIPアドレスが192.168.11.1になっていた。返送先のIPアドレスが重複していたため、VMWare Network Adapterの方へレスポンスが飛んでいたものと思われる(WireSharkでキャプチャしていたNICとは別デバイスなので、WireSharkに補足されない)。

対策

VMware Network Adapter VMnet8を無効化して、再度デバイスをTFTPでブートさせたところ、以下のように問題なくPumpKINからファイルが返送された。

[10/08/20 00:01:10] 'firmware_XXXX.XXX' of type 'octet' is requested from 192.168.11.168
[10/08/20 00:01:14] Transfer of 'firmware_XXXX.XXX' has successfully completed

匿名加工情報の作成に使う鍵つきハッシュの鍵をPBKDF2で生成する

個人情報を含むデータをもとにプログラムのテストデータを作成する場合などには、データの匿名化が必要になります。個人情報保護委員会個人情報の保護に関する法律についてのガイドライン(匿名加工情報編)」*1には、管理用IDなど特定の個人の識別に使える情報を加工する際は「乱数等の他の記述等を加えた上でハッシュ関数等を用いるなどの手法を用いる」よう記載されています。

噛みくだくと、たとえば会員データベースから匿名加工情報を作りたかったら

  • 会員IDなどの情報はHMAC-SHA256などの鍵つきハッシュでハッシュ化しろ
  • 鍵は匿名加工情報の提供先には渡すな
  • 複数の提供先に匿名加工情報を渡すならHMAC-SHA256の鍵は別々にしろ

ということだと思います。これは、SHA-256などで単純にハッシュ化した場合、匿名化した情報を受け取った人は以下のような攻撃が可能になってしまうからでしょうね。

  • たとえば、従業員番号をキーに、従業員の過去の行状(これは一般従業員には開示されておらず、管理職のみが参照できる)を記録したDBを匿名化するとします。匿名化処理自体は、DBの参照を許可されている人(この場合は管理職)が行うとします。
    • たとえば個人の従業員番号は社内の検索システムで検索可能だったとします。この場合、誰かの従業員番号をSHA-256でハッシュ化することで、その人の過去の行状を知ることができてしまいます。
    • 逆に、個人の従業員番号はその個人のみが知っているとします。この場合でも、匿名化されたDBを受けとった一般従業員は、自分の従業員番号をSHA-256でハッシュ化することで、開示されていないはずの自分の過去の行状を知ることができてしまいます。

……なんですが、鍵をファイルとして保存しようとするとその管理などがややこしい。そこで、鍵つきハッシュの鍵をPBKDF2で生成するという方法を考えてみました。

  • initKey()で鍵をセットします。鍵のセットに使うパスワードは、テストデータの作成担当者が覚えていればよいです(DBなどに保存する必要はありません)。名寄せされたらマズいデータが複数セットある場合は、データセットごとにパスワードは変えます。
    • 単にパスワードをSHA-256でハッシュ化して鍵とする方法も考えましたが、匿名化に使われたパスワードを推測されると鍵つきハッシュの意味がありません。ソルトをつけて、推測の容易性を下げます。ソルトは適当に長めの文字列を設定すればよいでしょう。なお通常、鍵の安全性はパスワードの強度で担保すると思うので、あくまでもソルトは補助です。
    • プログラムを逆コンパイルされた場合にはソルトが判明してしまうので、下記に記載の処理ではそれほど強い防御にはなりません(あくまで匿名化された情報を受け取る第三者によるカジュアルな攻撃の容易性を下げるだけ)。一応、パスワードとソルトを別々の人間が入力すれば、匿名化処理をした本人でも特定個人のデータを一本釣りできないデータを作ることはできます。ただ、匿名化処理をしている人は、そもそも生データにアクセスできているわけで、実務上はあまり意味がないですね。
  • 鍵をセットしたら、hash()で平文からハッシュを得ます。
  • initKey()を呼ぶのは最初の1回だけです。データがn件あったら、initKey()を1回呼んだあと、hash()をn回呼ぶことになります。initKey()はキーの生成に時間がかかります。複数回呼んでも結果は変わりませんが、速度が極端に遅くなると思います。
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.Scanner;

import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

public class Main {

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        try {
            System.out.print("Clear text: ");
            String clearText = sc.next();
            System.out.print("Password for hashing: ");
            String password = sc.next();
            Main main = new Main();
            main.initKey(password, "FIXED_SALT");
            System.out.println(main.hash(clearText));
        } finally {
            sc.close();
        }
    }

    /** Key derivation algorithm */
    private static final String KEY_DERIVATION_ALGORITHM = "PBKDF2WithHmacSHA256";
    /** Key derivation algorithm */
    private static final String HASHING_ALGORITHM = "HmacSHA256";
    /** How many times calculate */
    private static final int ITERATION = 1000;
    /** Key length */
    private static final int KEY_LENGTH = 256;
    /** key */
    private static byte[] key;

    /**
     * Generate key for HMAC from password and salt
     *
     * @param password clear password
     * @param salt salt
     */
    public void initKey(String password, String salt) {
        PBEKeySpec keySpec;
        SecretKeyFactory skf;
        SecretKey secretKey;
        try {
            keySpec = new PBEKeySpec(password.toCharArray(), salt.getBytes("UTF-8"), ITERATION, KEY_LENGTH);
            skf = SecretKeyFactory.getInstance(KEY_DERIVATION_ALGORITHM);
            secretKey = skf.generateSecret(keySpec);
            key = secretKey.getEncoded();
        } catch (UnsupportedEncodingException | NoSuchAlgorithmException | InvalidKeySpecException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Get hashed string with HMAC-SHA256
     * @param clearText string to be hashed
     * @return Hash (hex string)
     */
    public String hash(String clearText) {
        try {
            SecretKeySpec sk = new SecretKeySpec(key, HASHING_ALGORITHM);
            Mac mac = Mac.getInstance(HASHING_ALGORITHM);
            mac.init(sk);
            byte[] mac_bytes = mac.doFinal(clearText.getBytes("UTF-8"));
            StringBuilder sb = new StringBuilder(2 * mac_bytes.length);
            for(byte b: mac_bytes) {
                sb.append(String.format("%02x", b&0xff) );
            }
            return sb.toString();
        } catch (NoSuchAlgorithmException | InvalidKeyException | IllegalStateException | UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }
}

Microsoft Print to PDFで出力したPDFファイルが0バイトになる問題を回避する

大きなPDFファイルを複数に分割しようと思って、Adobe ReaderからMicrosoft Print to PDFへ印刷してみたんだけど、できあがるPDFファイルが0バイトで、正常に出力されない。 Wordでも同じ現象が起こったので、これはMicrosoft Print to PDFの問題かな?と思ったら、なんとAzure Information Protection Viewer(AIP)からは問題なくPDFを出力できる(なぜだ)。

じゃあWordファイルもAIPから出力すればいいじゃん!と思ったんだけど、今度はAIPでWordファイルを開けない。普通に対応してるって書いてあるんだけどなぁ……

docs.microsoft.com

というわけで、大きなPDFファイルを複数に分割したい場合は、以下の手順を踏むことにした。

  • 元々のファイルがPDFの場合
    1. AIPでPDFを開いて、印刷するページ範囲を指定してMicrosoft Print to PDFへ出力する。
  • 元々のファイルがMicrosoft Officeファイルである場合
    1. Officeの機能でファイルをPDFへエクスポートする。
    2. AIPでPDFを開いて、印刷するページ範囲を指定してMicrosoft Print to PDFへ出力する。

cmd.exeとPowerShellの起動速度

体感ではPowerShellの起動速度はcmd.exeと比べて明らかに遅いけれど、具体的にどの程度遅いのか確かめてみた。 OSはWindows 10 version 2004、CPUはIntel(R) Core(TM) i7-8750H CPU @ 2.20GHz(6コア12スレッド)です。 なお、処理に使うウィンドウはPowerShellのものでもcmd.exeのものでも起動速度は変わりませんでした。

# 起動元 起動先 起動回数[回/秒] コマンド
1 cmd.exe cmd.exe 82~84 FOR /L %n IN (1,1,300) DO cmd /V:ON /C "echo !TIME!"
2 cmd.exe powershell.exe 4~5 FOR /L %n IN (1,1,300) DO cmd /V:ON /C powershell.exe -c "Get-Date -Format 'yyyy/MM/dd hh:mm:ss.fff'"
3 powershell.exe cmd.exe 75~77 1..300 | ForEach-Object { cmd /V:ON /C "echo !TIME!" }
4 powershell.exe powershell.exe 4~5 1..300 | ForEach-Object { powershell.exe -c "Get-Date -Format 'yyyy/MM/dd hh:mm:ss.fff'" }

ファイルをAESで暗号化したときに送信先とnonceを共有する方法

AESをECBモードで使っていたり、IVとして固定値を使っていたりするケースを避けるお手本として

  • あるファイルをAESで暗号化し、ローカルマシンのストレージ上に保存しておく
  • 暗号化したファイルをリモートマシンへ送付し、リモートマシンのストレージに保存する
  • あるタイミングでリモートマシン上でファイルを復号する

という処理を実装しようとしたときに、AESで使うIVとかnonceをどう生成し、ローカルマシンとリモートマシンでどう共有するか?という話をまとめておく。なお、以降では話を簡単にするためAESはCTRモードで使用する(nonceを使う)ものとする。nonceは再利用を避けられればそれでよい。nonceの値は推測可能でもよい *1

nonceの生成方法

NIST SP 800-38A *2 では、nonceおよびIVの作成方法として以下の(A)(B)2つの方法が挙げられている。またRFC 5297 *3 ではnonceとしてカウンタを使う方法の他に、タイムスタンプを使う方法(C)が提示されている。

  • (A) カウンタを暗号化する*4
  • (B) FIPS 140-2準拠の乱数を使う
  • (C) タイムスタンプを使う

まず、nonceの生成のためにカウンタを保存する方法(A)は実装や管理の煩雑さから避けたい。末番を保存しておいてそれをカウントアップする方法は、どこに末番を保存するか?末番の採番を並列化可能にするにはどうするか?といった検討が必要になってしまう*5

方法(B)と(C)に大きな違いはないが、(B)には誕生日のパラドックスに起因してnonceの衝突確立が(C)より高いという問題がある。また、完全に重複を避けるには使った乱数をすべて保存しておく必要がある。そのため、ここではいったん(B)を除外する。

ここでは(C)を利用することにする。たとえばシステムクロックのミリ秒までの値をnonceとして利用する。Java 9以降であれば、Instant.now()でシステムクロックを取得すれば、getEpochSecond()とgetNano()でナノ秒単位の時刻が取得できる。システムクロックは一方向にしか進まないため、ある時点でnonceの生成処理が複数同時に実行されることさえ防げば、nonceが再利用されないことを担保できる。

nonceの共有方法

ファイルの復号のため、nonceはローカルマシンとリモートマシンとで共有する必要がある。ここで、ファイルをリモートマシンへ送付する際、nonceを別ファイルに格納して送るのは必要な処理が増えるし、処理の原子性の考慮が難しくなるため避けたい。

ローカルマシンとリモートマシンのシステムクロックの値をnonceとして利用することで、ローカルマシンからリモートマシンへnonceを送信せずにnonceを共有できるのでは?とも考えたけど、暗号化するタイミングと復号するタイミングは異なるはずなので、そもそもこの方法は使えない。仮に使えたとしても、どうしてもローカルマシンとリモートマシンとでnonceの値がズレるタイミングが発生する *6 。また、同時刻に実行された処理で同一のnonceが使われるのを防ぐことも難しい。

nonceはファイルごとに決める必要があり、またファイル間でnonceの値は異なっている必要がある。これはファイル名の特性と同じである *7。そのため、ファイル名にnonceの値を含めて送信することで、ローカルマシンとリモートマシンとでnonceを共有することができる。

nonceの一意性を担保する方法

nonceを生成して暗号化されたファイルを作る際、もしnonceと同じ名前のファイルがすでに存在していたら、そのnonceは使用済ということになる。その場合は再度nonceを取得すればよい。

*1:暗号強度は鍵のビット長で担保される

*2:https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38a.pdf

*3:https://tools.ietf.org/html/rfc5297

*4:カウンタが暗号ブロック長より短かければECBモードで暗号化しても問題にならない

*5:テストの再現性の面では利点もあるが、それはnonceの生成処理をinject可能にしておけばよい話で、カウンタが必須なわけではない

*6:たとえばYYYYMMDDHHMMSSをnonceにするとして、ローカルマシンの時刻が2020/01/01 01:01:01.999、リモートマシンの時刻が2020/01/01 01:01:02.001とすると、NTPによる時刻同期の精度としては上出来だがnonceとしてはローカルマシンとリモートマシンでズレてしまっているので役に立たないことになる

*7:ファイル名はファイルごとに決める必要があり、同名のファイルは保存できない