Lazy Diary @ Hatena Blog

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

匿名加工情報の作成に使う鍵つきハッシュの鍵を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);
        }
    }
}