理系学生日記

おまえはいつまで学生気分なのか

Socket

最近ソケット通信実装のサポートに入っていたので、ちょっとまとめてみたいと思います。

Socket からのデータ読込

Java に関しては不勉強なところが多いんですが、Socket クラスは「ソケット」を抽象化したものであり、「ソケット」は TCP におけるコネクションの端点を抽象化したものです。 結果として、Socket クラスには、TCP における端点に必要な機能が抽象化されています。

一般に Java で Socket から生のバイトを読み込む場合はこういうコードになると思います。これは TCP のコネクションを都度接続する前提です。

byte[] buf = new byte[tlgrmSize];
InputStream is = new BufferedInputStream(socket.getInputStream());

// ByteArrayOutputStream baos = new ByteArrayOutputStream(tlgrmSize);
while((len = is.read(buf, 0, remaining)) != -1) {

    // do something.
    // ex., baos.write(buf, 0, len);

    remaining = hogehoge() // 読み込むべき残りバイトを計算
}

つまりは、以下のような事柄を、InputStream#read で -1 (EOF) が返ってくるまで続けます。 読み込むべき残りバイトを計算するか否かは要件に依存します。

  1. 受信用の byte[] バッファを生成
  2. Socket#getInputStream() で得られる InputStreamread メソッドで、データを byte[] バッファに読み込み
  3. byte[] バッファに読み込んだバイトに対して必要な処理を行う (ここでは、ByteArrayOutputStream に書き込んでいます)
  4. 読み込むべき残りバイトを計算する

このあたりの処理フローは、Java だろうが C だろうがあまり変わらないと思います。

注意すべきは、InputStream#read メソッドで、要求したデータサイズが一度に読み込めないケースが多々ある点で、これは考慮しておかなければなりません。 上記コードですと、InputStream#read の第三引数で指定した「読み込む最大バイト数」よりも実際に読み込んだバイト数 (read の返却値) が小さい場合があります。

バッファサイズとウィンドウサイズ

上記の受信用バッファのサイズをどのくらい取れば良いのか、という話もあったのですが、このサイズは正直、あまりに大きなメモリ負荷を与えないのならどのくらいでも良いと思っています。 大した流量もなく、読み込むべきデータサイズが既知であり、かつ、それが数 KB に収まるなら、そのサイズくらい一気に取ってしまえば良いんじゃないですかね。

最初に見たコードは TCP のウィンドウサイズを意識したバッファサイズ設計になっていたというのが上の話に繋がるんですが、 Java の Socket において TCP のウィンドウサイズを意識する必要はないと思います。そもそも、TCP のウィンドウサイズの情報は Socket からは入手できないよね。

TCP のウィンドウサイズは大きく 2 つ存在していて、これらのサイズは通信状況に依って動的に調整されます。

  • フロー制御におけるウィンドウサイズ
    • TCP において、受信側の受信バッファをオーバーフローさせないためのウィンドウサイズ。
  • 輻輳制御におけるウィンドウサイズ

どのタイムスライスにおいて、どの程度のデータを送られ得るかは、両ウィンドウやその他の状況によって複雑に調整されます。この手の情報を抽象化するのは Kernel の役割です。

JavaでのQRコード生成

QR コード

あまり知られていませんが、QR コードは日本発の規格になります。 スマートフォンで打ち込むのが面倒なデータをカメラ読込のみで入力できることから世界的にも普及しておりまして、先日は iOS の Chrome に QR コードをスキャンする機能も追加されたのは記憶に新しいところです。

Java における QR コードライブラリ

Java で QR コードを作る場合、zxing (Zebra Crossing) を使用するのがスタンダードのようです。 これとは別に、QRGenというライブラリもあるのですが、こちらも内部で zxing を使用しています。QRGen については、すごく使いやすいインタフェースを提供してくれているので、そちらを使うという選択もアリだとは思います。

本日(2017/03/11) 時点でのデータは以下のとおり。

ライブラリ ライセンス Star 最終 commit
zxing Apache License 2.0 12,454 2017/03/08
QRGen Apache License 2.0 593 2016/10/31
Barcode4J Apache License 2.0 - 2012/????

zxing を用いた実装

最初に、作成した画像をそのままブラウザに返却する想定の実装例です。これにより、以下のような QR コードが生成されます。

    // QR コードで表現するコンテンツ
    final String contents = "kiririmodeのQRコード";

    // QR コード生成用のオプション
    final Map<EncodeHintType, Object> hints = new EnumMap<>(EncodeHintType.class);
    hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);
    hints.put(EncodeHintType.MARGIN, 2);
    hints.put(EncodeHintType.CHARACTER_SET, StandardCharsets.UTF_8);

    try {
      // QR コード用一時画像ファイルの作成
      final Path path = Files.createTempFile("qr", ".png",
          PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-------")));

      // QR コードデータの生成
      final BitMatrix matrix =
          new QRCodeWriter().encode(contents, BarcodeFormat.QR_CODE, 1024, 1024, hints);
      // QR コードを画像化
      MatrixToImageWriter.writeToPath(matrix, "PNG", path);

      final HttpResponse response = new FileResponse(path.toFile(), true);
      response.setContentType("image/png");
      return response;
    } catch (WriterException | IOException e) {
      throw new RuntimeException(String.format("converting [%s] to qrcode fails", contents));
    }

QR コード生成用のオプションは、個々に EncodeHintType として表現します。

エラー訂正のレベル

まず、ERROR_CORRECTION はエラー訂正のレベルになります。QR コードは wikipedia:リードソロモン符号 をベースとしており、誤り訂正を行うことができます。

エラー訂正レベルを上げるほど、一部が隠れていたり滲んでいたりしても QR コードとして読み取ることができますが、そのぶんデータ量が多くなります。データ量が多くなれば、クライアント側の読み取り速度にも影響を与えることがあります。 デンソーに依れば、エラー訂正レベル M (15 % まで訂正可能) で使われることが多いようです。

文字コード

また、文字コードのデフォルトは、ISO-8859-1 なので、日本語が表現できません。日本語を QR コードに埋め込みたい場合は、実装例のように CHARACTER_SET に対して UTF-8 等を明示的に指定する必要があります。

バージョン

QR コードにはバージョンがありますが、これは仕様としてのバージョンではなく、その QR コードのサイズによって分類されています。 そのサイズによって当然ながら QR コードに含められる情報量は変化します。ただ、zxing はそのあたりの面倒を自動的に見てくれる (コンテンツのデータサイズによって、自動的にバージョンを判別し、それに応じたサイズの QR コードを出力してくれる) ので、実装上あまり悩むことはないと思います。

全体として

全体として、zxing はすごく使いやすいライブラリだと思います。 最初だけ、どこから手をつけれんば良いんや…という状態でしたが、色々なサンプル実装を見た後で JavaDoc を読み解けば、なるほどこうすれば書けるのかというのが掴めてきます。

個人で実装するのは相当つらそうだったので、有り難く使わせてもらおうと思います。

JavaのJSSEでクライアント証明書を自由に選択できるようにする

HTTPS で API を呼び出すっていうシーンは頻繁にあって、その API を使うには、通常何らかの認証が求められます。 認証にも色々あるんだけど、そのうちの一つが HTTPS のクライアント認証です。 普通に HTTPS 通信をしたいだけだと意識しないことも多いのですが、宛先サーバ毎にクライアント証明書を使い分ける、なんてニーズが生じたときにはこのあたりの理解をしておくことは不可避になります。

こういうのを Java でやれって言われたときに、エッどうやって実現するの、みたいなかんじだったので、調べたり実装してみて調べた結果をまとめてみます。

前提知識としては、以下のようなことが分かっていれば良いと思います。

知識編

2 つの証明書ストア (TrustStore と KeyStore)

まず、Java における SSL/TLS の実装は JSSE (Java Secure Socket Extension) によって提供されるのですが、ここで KeyStore と TrustStore という用語が出てきます。 KeyStore と TrustStore はすごく重要な概念なんですが、異常に混乱を招きがちだし、そもそも JSSE のリファレンスガイドからしてこんなの分かんねーよという説明しかされていない。なんだこれ、暗号か?

キーストアは、鍵データのデータベースです。鍵データにはさまざまな用途があり、それには認証やデータ整合性も含まれます。利用できるキーストアには様々なタイプがあり、その中にはPKCS12やOracleのJKSも含まれます。 (略) トラストストアとは、トラストの対象を決めるときに使用するキーストアです。すでに信頼しているエンティティからデータを受け取る場合、およびそのエンティティが発信元を名乗るエンティティであることを確認できる場合は、データは実際にそのエンティティから届いたものであると仮定できます。

でも、これらが分かっていないとクライアント認証だったりは実現できないし、そもそも JSSE がなかなか読み解けない。 そういうわけですから、まずは TrustStore と KeyStore を整理してみます。

SSL においては、通常、サーバ証明書によって、通信を行うサーバが「間違いなく自分の通信したいサーバであること」(逆に言えば、悪意のある第三者が用意したサーバでないこと)を確認することになります。PKI の仕組み上、このためには、サーバがクライアントにサーバ証明書を提示し、クライアントが「信頼してる機関 (CA) がこのサーバ証明書を(少なくとも間接的に)発行している」という判断が必要です。 逆にクライアント証明では、クライアントがサーバに対してクライアント証明書を提示し、サーバが「信頼してる機関 (CA) がこのクライアント証明書を(少なくとも間接的に)発行している」と判断することが必要になります。

このために、クライアント側では以下の情報を保持しておく必要があります。

  1. 自分がどの証明書(CA 証明書、サーバ証明書) を信じるのか
  2. 自分がどのクライアント証明書をサーバに提示するのか

この 1. の情報を保持しておくのが TrustStore、2. の情報を保持しておくのが KeyStore です。TrustStore は Trust、つまり自分の信頼するものが何かを蓄えるもの、KeyStore は自分が提示する秘密情報 (Key) を蓄えるもの、って考えるとちょっと覚えやすくなるかもしれません。 ちなみに、この TrustStore と KeyStore はどちらも Java でいう "KeyStore" とよばれるファイル(実際にはファイルでなくても良いですが、ファイルの形を取ることが多い) に保存されたりするのが話をややこしくする元凶なんじゃないかと思います。

まぁこのあたりは JSSE よりも キーストアとトラストストア (SSL をサポートする Java CAPS の構成) の記述もわかりやすいですね。

JSSE では、「キーストア」および「トラストストア」と呼ばれるファイルを使用します。キーストアは、アダプタでクライアント認証に使用され、トラストストアは、SSL 認証でサーバーを認証する際に使用されます。

要するに、TrustStore が相手が正しいかを検証するのに使用するもので、KeyStore は自分が正しいかを相手に伝えるのに使用するものです。どちらも証明書を保持しています。

TrustManager と KeyManager

TrustStore、KeyStore はともにストレージみたいに証明書を保存するためのものなので、それを使う人が必要です。

TrustStore を使って相手から送られてきたサーバ証明書が信頼できるかを検証する責務を負うのが TrustManager、 KeyStore を使って、どのクライアント証明書を相手に送付するかを決めるのが KeyManager です。

これらはともにインタフェースとして定義されていて、自由に実装をつくることができます。 ただ、実際にこれらの実装を自分でインスタンス化するってことは滅多になくて、これらのファクトリクラスである KeyManagerFactory や TrustManagerFactory を作成し、これらから KeyManager、TrustManager を作成するってことが大半です。

SSL についての情報を保持する SSLContext

この図は、JSSE のリファレンスガイドに登場する図なんですが、前述の TrustManager や KeyManager を使って、SSLSocket (名前の通り SSL のソケットを表現するクラス) を作る SSLSocketFactory を構築するのが SSLContext クラスです。この SSLContext は、SSL 実装に関する情報を保持しています。

f:id:kiririmode:20160612022046j:plain

クライアント認証に関する何らかの操作を実現しないといけない場合、往々にして KeyManager をカスタマイズする必要がありますが、カスタマイズした KeyManager を使ってくれ、ということを SSLContext に教える必要があります。 Apache の httpComponent などの HTTP クライアント実装も、だいたいこの SSLContext を差し替えることができるようになっているので、その差し替えによってクライアント証明書の選択とかを HTTP クライアントライブラリ側に反映させることができます。

httpComponent だと以下のようなかたちで、httpClient を作成する段階で SSLContext を自作のものに設定できます。

try (CloseableHttpClient client = HttpClients.custom().setSSLContext(sslContext).build()) {
  HttpGet getMethod = new HttpGet("https://server.kiririmode.com:14433/index.html");

  client.execute(getMethod);
}

デフォルトのクライアント証明書の選択

さて、それではクライアント証明書がデフォルト実装でどのように実装されるかを見てみました。 あくまで java.security をカスタマイズしていない前提ですが、何も意識しないで HTTPS 通信を行った場合、クライアント証明書は以下の条件に合致するものが選択されます。

  1. KeyManager が読み込む KeyStore に格納されているクライアント証明書である
  2. サーバが Certificate Request の中で送信してくる信頼する CA が発行している
  3. 鍵のアルゴリズムも合致する

複数のクライアント証明書が上記の条件に合致した場合は、最初に発見されたものが選択されます。 つまり、同じ CA が発行しているようなクライアント証明書が複数あった場合、対向先システムとか関係なしにそのうちの 1 つが選択されてしまうってことになります。 この振舞いについては、割と不都合あるケースが多いんじゃないでしょうか。

実装編

というわけで、上記のような基本知識を元にして、「クライアント証明書を対向サーバ毎に別々に選択する」っていうのを作ってみます。

クライアント証明書を自由に選択できる KeyManager

最初に作らないといけないのは、KeyManager の実装なんですが、実際にはクライアント証明書の形式としては通常 X.509 しかないはずなので、X509Keymanager インタフェースを実装する形で良いと思います。 クライアント証明書を選択するのは chooseClientAlias メソッドなので、これのみ実装を差し替えれば良い。というわけで、移譲させるようにします。

実装自体は、以下のとおりクライアント証明書を選択するのは Strategy パターンにしておきました。

@FunctionalInterface
public interface AliasSelectionStrategy {
  
  String selectAlias(String[] keyType, Principal[] issuers, Socket socket);

}
public class StrategyKeyManagger implements X509KeyManager {
  
  private X509KeyManager defaultKeyManager;
  private AliasSelectionStrategy aliasSelectionStrategy;
  
  public StrategyKeyManagger(X509KeyManager defaultKeyManager, AliasSelectionStrategy strategy) {
    this.defaultKeyManager = defaultKeyManager;
    this.aliasSelectionStrategy = strategy;
  }

  @Override
  public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
    String alias = aliasSelectionStrategy.selectAlias(keyType, issuers, socket);
    return (alias != null)? alias : defaultKeyManager.chooseClientAlias(keyType, issuers, socket);
  }

  @Override
  public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
    return defaultKeyManager.chooseServerAlias(keyType, issuers, socket);
  }
  // 以下略

SSLContext の差し替え

あとは、この KeyManager を使用して SSL 通信を行うように SSLContext を差し替えれば良いです。 これは以下のステップで実施します。

  1. デフォルトで使用される KeyManager を取得する
  2. そのうち、X509KeyManager の実装クラスを上記で作成した StrategyKeymanager に差し替える
  3. 差し替えた KeyManager で SSLContext を初期化する
  private static SSLContext getSSLContext(
      String keyStorePath, String keyStoreType, char[] keyStorePassword, AliasSelectionStrategy strategy,
      String trustStorePath, String trustStoreType, char[] trustStorePassword
      ) throws IOException, GeneralSecurityException {
    
    TrustManager[] trustManagers = getTrustManager(trustStorePath, trustStoreType, trustStorePassword);
    // 1. デフォルトで使用される KeyManager を取得する
    KeyManager[] keyManagers = getKeyManagers(keyStorePath, keyStoreType, keyStorePassword);
    
    // 2. KeyManager を差し替える
    for (int i = 0; i < keyManagers.length; i++) {
      if (keyManagers[i] instanceof X509KeyManager) {
        keyManagers[i] = new StrategyKeyManagger((X509KeyManager)keyManagers[i], strategy);
        break;
      }
    }
    
    // 3. 差し替えた KeyManager で SSLContext を初期化する
    SSLContext sslContext = SSLContext.getInstance("SSL");
    sslContext.init(keyManagers, trustManagers, null);
    return sslContext;
  }
    
  private static KeyManager[] getKeyManagers(String keyStorePath, String keyStoreType, char[] keyStorePassword) throws IOException, GeneralSecurityException {
    String algorithm = KeyManagerFactory.getDefaultAlgorithm();
    KeyManagerFactory kmf = KeyManagerFactory.getInstance(algorithm);
    
    KeyStore keyStore = null;
    try (InputStream is = Files.newInputStream(Paths.get(keyStorePath))) {
      keyStore = KeyStore.getInstance(keyStoreType);
      keyStore.load(is, keyStorePassword);
    }
    
    kmf.init(keyStore, keyStorePassword);
    return kmf.getKeyManagers();
  }    

クライアント証明書の選択の例

ここでは、対向サーバのホスト名毎にクライアント証明書を切り替える実装を作ってみました。

public class HostBasedAliasSelectionStrategy implements AliasSelectionStrategy {

    private Map<String, String> aliasMap;

    public HostBasedAliasSelectionStrategy(Map<String, String> hostAliasMap) {
        this.aliasMap = hostAliasMap;
    }

    @Override
    public String selectAlias(String[] keyType, Principal[] issuers, Socket socket) {
        // 対向ホスト名のクライアント証明書用 alias が Map に登録されていれば、それを使用する
        String hostName = socket.getInetAddress().getHostName();
        if (aliasMap.containsKey(hostName)) {
            return aliasMap.get(hostName);
        }
        return null;
    }
}

組み合わせる

あとはこれらを使って、HTTPS 通信をすれば良いでしょう。

ここでは、server.kiririmode.com 宛の通信には、client1 という alias で登録されたクライアント証明書が使用されます

  public static void main(String[] args) throws Exception{
    
    // server.kiririmode.com 宛の通信には、`client1` という alias で登録されたクライアント証明書を使用する
    Map<String, String> aliasMap = Collections.singletonMap("server.kiririmode.com", "client1");
    HostBasedAliasSelectionStrategy strategy = new HostBasedAliasSelectionStrategy(aliasMap);
    
    SSLContext sslContext = getSSLContext(
        "/path/to/keystore.ks", "JKS", "password".toCharArray(), strategy,
        "/path/to/truststore.ks", "JKS", "password".toCharArray()
        );
    try (CloseableHttpClient client = HttpClients.custom().setSSLContext(sslContext).build()) {
      HttpGet getMethod = new HttpGet("https://server.kiririmode.com:14433/index.html");
    
      client.execute(getMethod);
    }

参考文献

  1. JSSEリファレンス・ガイド
  2. Difference between trustStore and keyStore in Java - SSL
  3. Article: Creating Custom Key Managers
  4. GitHub - IBM/japan-technology: IBM Related Japanese technical documents - Code Patterns, Learning Path, Tutorials, etc.

Stubby4JにおけるJSONでの正規表現、あるいはリクエスト中の特定のフィールドでレスポンスを変更する

Stubby4J を使用したテストにおいて、API リクエストとして送る JSON の中の値によって、レスポンスを切り替えたい場合がある。 もうちょっと具体的にいうと、

  1. リクエストの JSON の中の特定のフィールドの値によってのみ、レスポンスを切り替える
  2. 他のフィールドの値は実行時まで予想できない

こういうケースでレスポンスを切り替えるのは Stubby4J (v3.3.0) ではできないということが分かったので、その原因と、何とかする方法を考えてみる。

実装上できない理由

Stubby4J では、実際に送られてきた HTTP リクエストと、設定ファイル(YAML) 上に記述されたリクエストを StubRequest#equals(Object) で突合し、合致する場合にのみ、YAML ファイル上に記述されたレスポンスを返却するという動作になっている。 この「突合」(equals) の実装は、実際の HTTP リクエストの Content-Type によって挙動を替えるようになっている。application/json の場合は、「送信されてきた JSON」と「YAML ファイル上に記述された JSON」が、JSON のフィールド順を除いて完全一致すること が条件になっている。 以下に実装を示すけど、JSONCompare を使用しているということは、HTTP リクエスト上の JSON はもちろん、YAML 上に記述した JSON も valid である必要があるので、何らかの細工 (正規表現を記述するなど) もできない。

   private boolean postBodiesMatch(final boolean isDataStorePostStubbed, final String dataStorePostBody, final String thisAssertingPostBody) {
         // snip

         } else if (isSet(assertingContentType) && assertingContentType.contains(Common.HEADER_APPLICATION_JSON)) {
            try {
               return JSONCompare.compareJSON(dataStorePostBody, thisAssertingPostBody, JSONCompareMode.NON_EXTENSIBLE).passed();
            } catch (JSONException e) {
               return false;
            }
         } else if (isSet(assertingContentType) && assertingContentType.contains(Common.HEADER_APPLICATION_XML)) {

         // snip
   }

設計思想によってできない理由

Json request body matching by vishalmanohar · Pull Request #19 · azagniotov/stubby4j · GitHub で、JSON の中の値でレスポンスを切り替えられるような Pull Request があった。 これを利用すると、以下のようにリクエストの JSON に依存したレスポンスの設定がかけるようになる。

-  request:
      method: POST
      url: /templatize/response
   response:
      -  status: 200
         headers:
            content-type: application/json
         body: >
               <#assign requestBodyJson = requestBody?eval>
               {"foo": "${requestBodyJson.foo}"}

のだけれど、この Pull Request に対しては「HTTP リクエストと YAML 上のリクエストの突合は HTTP 1.1 レベルでしか実施しない」という設計思想が語られており、Merge されないままクローズされた。 実はこのあとに Content-Type に依存した突合の実装が取り込まれていることから鑑みるに、設計思想が変化しているみたいなのだけれど…。

何とかする方法

上記の PR と同じような修正を行ったり、あるいは現行の実装で使用している JSONCompare での比較ではなく、もっと柔軟な比較 (正規表現を許容する、jsonpath で比較すべきフィールドを指定する、JSON Schema を書けるようにする、等) をできるようにする、などはできるのだけど、とりあえず実現するだけなら以下で可能になる。

$ git diff  main/java/by/stub/yaml/stubs/StubRequest.java
diff --git a/main/java/by/stub/yaml/stubs/StubRequest.java b/main/java/by/stub/yaml/stubs/StubRequest.java
index 7e6a8a8..d76d6de 100644
--- a/main/java/by/stub/yaml/stubs/StubRequest.java
+++ b/main/java/by/stub/yaml/stubs/StubRequest.java
@@ -252,12 +252,6 @@ public class StubRequest {
          final boolean isAssertingValueSet = isSet(thisAssertingPostBody);
          if (!isAssertingValueSet) {
             return false;
-         } else if (isSet(assertingContentType) && assertingContentType.contains(Common.HEADER_APPLICATION_JSON)) {
-            try {
-               return JSONCompare.compareJSON(dataStorePostBody, thisAssertingPostBody, JSONCompareMode.NON_EXTENSIBLE).passed();
-            }

これはかなり強引な方法で、いってみれば「JSON とか知るか!! 文字列として扱うんや!!! 」という方法である。 つまり、リクエストが valid JSON でなくても、XML や Form であってもマッチしてしまうので、YAML 上でリクエストを記述するときにかなりの注意を払わなければならない。

上記修正を行った後で、gradle build -x test でビルドして実験し、以下のような YAML で Stubby4J を立ち上げる。

- request:
    method: POST
    url: /hello-world
    post: >
      \{\s*"hello"\s*:\s*"(.*?)"\s*.*\}

  response:
    status: 200
    headers:
      content-type: application/json
    body: Hello, <% post.1 %>

これに対して、リクエストを送信すると、きちんとレスポンスを切り替えられるようになる

$ curl -H'content-type: application/json' -d "{\"hello\":\"kiririmode\", \"hoge\":\"fuga\" }" http://localhost:8882/hello-world
Hello, kiririmode
$ curl -H'content-type: application/json' -d "{\"hello\":\"world\", \"var\":\"val\" }" http://localhost:8882/hello-world
Hello, world

Maven Assembly Plugin でできるファイル名がクソダサかった話

Maven Assembly Plugin でできるファイル名がクソださかったのでなんとかしたかった話。

descriptorRef 要素で jar-with-dependency 指定してたら、hoge-0.0.1-SNAPSHOT-jar-with-dependencies.jar とかいうフザけた名前になってて、あまりの衝撃に手足が震えはじめ唇が紫に変色した。

いきなし結論から書くと、まず pom.xml<configuration> に対して、<finalName> を指定する。

<finalName>vault-${project.version}</finalName>

これだけだと、<finalName> で指定した名前に対して、assemblyId が付与されてしまうので、さらに <appendAssemblyId>false</appendAssemblyId> を付与する。 なので、pom.xml の指定としては以下のようになる。

<finalName>vault-${project.version}</finalName>
<appendAssemblyId>false</appendAssemblyId>

こうすることで、Assembly Plugin で生成されるファイル名を vault-0.0.1-SNAPSHOT.jar とかにできる。これにより手の震えが治まる。

Maven Assembly Pluginで実行可能jarをつくる

maven assembly plugin

maven 力も Java 力も貧弱なのだけれど、依存性を 1 つにまとめた実行可能 jar を作る必要性に駆られたので、ちょっと調べてた。 maven の plugin で実行可能 jar を作ろうとすると、以下の 2 つがメジャー。

  1. Maven Assembly Plugin
  2. Maven Shade Plugin

両方ともに、いわゆる uber-jar とか Fat Jar とか言われている、依存関係をすべて 1 つにまとめた Jar を作成することはできるのだけれど、それを主目的と考えると後者の Maven Shade Plugin の方が合致する。これは主に以下のような機能によるものらしいが、bytecode を置き換えるとかいうヤバげな文字も見えたので、そっ閉じした。

というわけで、Maven Assembly Plugin をまずは使いましょう。

assembly とは何か

Maven Assembly Plugin の言う "assembly" というのは、ファイルやディレクトリ、モジュールが依存している jar などを含めたアーカイブのことで、これを作り出すことが maven assembly plugin の目的。

"assembly" の形式

plugin に予め定義されている assembly の形式としては以下のような 4 つがあって、maven の descriptorRef で記述することができる。

  • bin
    • mvn package で作成される jar ファイルと、README、LICENSE、NOTICE といったファイルをアーカイブしてくれる。フォーマットとしては、tar.gztar.bz2zip が選択できる。 もうちょっと具体的に言うと、${project.basedir} 配下の README*LICENSE*NOTICE* と、${project.build.directory} 配下の jar ファイルがアーカイブされる。
  • jar-with-dependencies
    • 依存している jar ファイルを含めた "uber-jar" を作成してくれる。uber-jar については後述するけど、いわゆる fat jar のこと。
  • src
    • こっちはその名の通り、pom のプロジェクトのソースファイル群をアーカイブしてくれる。 もうちょっと具体的に言うと、${project.basedir} 配下の README*LICENSE*NOTICE*pom.xml${project.basedir}/src 配下のファイルが格納される。
  • project
    • pom で管理しているプロジェクトをそのまま assembly にする。
    <configuration>
      <descriptorRefs>
        <descriptorRef>jar-with-dependencies</descriptorRef>
      </descriptorRefs>
    </configuration>

もちろん、今回の目的に叶うのは jar-with-dependencies ではあるのだけれど、この「予め定義されている」descriptor だと今後の細かなカスタマイズができない。 そういう場合は、Assembly Descriptor を自作すれば良い。

依存関係をすべて含めた Assembly Descriptor

Assembly Descriptor はその名前のとおりで "Assembly" がどういうものかを記述するもので、基本的には「どのファイルを」「どこに配置する」Assembly なのかを記述していけば良い。 ぼくの用途はいまのところ jar-with-dependencies とほとんど変わらず、

  1. runtime スコープにある依存 jar をすべて含む

ということくらいしかやることないので、以下のようなファイルをつくって、vault-assembly.xml とかで保存して src/assembly 配下に保存してやった。

<assembly
    xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 http://maven.apache.org/xsd/assembly-1.1.3.xsd">
    <id>vault</id>
    <formats>
        <format>jar</format>
    </formats>
    <includeBaseDirectory>false</includeBaseDirectory>
    <dependencySets>
        <dependencySet>
            <outputDirectory>/</outputDirectory>
            <useProjectArtifact>true</useProjectArtifact>
            <unpack>true</unpack>
            <scope>runtime</scope>
        </dependencySet>
    </dependencySets>
</assembly>

Manifest ファイル

実行可能 jar をつくるためには Manifest ファイルの Main-Class エントリを作成する必要があるけど、Maven Assembly Plugin には archive の設定でこれを記述することができる。

なので、Assembly Descriptor を含め pom.xml に指定してやれば良い。

<configuration>
    <finalName>vault-${project.version}</finalName>
    <appendAssemblyId>false</appendAssemblyId>
    <descriptors>
        <descriptor>src/assembly/vault-assembly.xml</descriptor>
    </descriptors>
    <archive>
        <manifest>
            <mainClass>com.kiririmode.vault.util.Main</mainClass>
        </manifest>
    </archive>
</configuration>
<executions>
    <execution>
        <id>make-assembly</id>
        <phase>package</phase>
        <goals>
            <goal>single</goal>
        </goals>
    </execution>
</executions>

#

参考文献

Stubby4JをJUnitから利用する

Stubby4J を Junit で起動できればだいたいの HTTP 要求に関するテストはできる。
だいたいこんな感じでテストを書きたい。

public class Stubby4jTest {
    
    @ClassRule
    public static Stubby4jServer server 
        = Stubby4jServer.fromResource("com/kiririmode/blog/http/client/stubby4j.yaml");

    @Test
    public void test() throws Exception {
        ClientResponse<?> response = new ClientRequest("http://localhost:8882/hello-world").get();
        assertThat(response.getEntity(String.class), is("Hello World!"));
    }
}

よくよく考えると Java で書かれていて Jetty 組み込みのサーバなんだし悩むことなかった。 Stubby4J の API 見ると Jetty 起動する API も既に用意されてた。

public class Stubby4jServer extends ExternalResource {

    private String yamlConfigurationFileName;
    private StubbyClient stubbyClient;

    public static Stubby4jServer fromResource(String resource, boolean isMute) {
        String yamlConfigurationFileName = Stubby4jServer.class.getClassLoader().getResource(resource).getPath();
        return new Stubby4jServer(yamlConfigurationFileName, isMute);
    }

    public static Stubby4jServer fromResource(String resource) {
        return fromResource(resource, true);
    }

    private Stubby4jServer(String yamlConfigrationFileName, boolean isMute) {
        this.yamlConfigurationFileName = yamlConfigrationFileName;
        ANSITerminal.muteConsole(isMute);
    }

    @Override
    protected void before() {
        try {
            stubbyClient = new StubbyClient();
            stubbyClient.startJetty(yamlConfigurationFileName);
        } catch (Exception e) {
            throw new RuntimeException(
                    String.format("stubby4J start failed.  conf=[%s]", yamlConfigurationFileName),
                        e);
        }
    }

    @Override
    protected void after() {
        try {
            stubbyClient.stopJetty();
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage(), e);
        }
    }

}

RestEasyとHttpComponents、そしてConnectionManager

RestEasy client を使用した HTTP 要求を行うと、TCP コネクションが長時間にわたり残存する問題が発生することがあります。 RestEasy の公式ドキュメントを読めば「HTTP コネクションは適切に release し、適切に close しなければならない」ということは分かるのですが、RestEasy にてどのように HTTP コネクションが管理されているのか良く分からなかったので、そのあたりを調べてみました。

RestEasy と HttpComponents

RestEasy が HTTP 要求を行う際、デフォルトでは、HttpComponents を内部で使用することになっています。 このあたりは、RestEasy における HTTP 要求を示すクラスである ClientRequest の実装を見れば分かりやすいでしょう。 何も意識しないと、以下のコンストラクタを使うことになりますが、ここではデフォルトの ClientExecutorgetDefaultExecutor() で生成しています。

   public ClientRequest(String uriTemplate)
   {
      this(uriTemplate, getDefaultExecutor());
   }

では、その実装はというと、以下のように、ApacheHttpClient4Executor を使用するようになっていることがわかります。

   private static String defaultExecutorClasss = "org.jboss.resteasy.client.core.executors.ApacheHttpClient4Executor";

   public static ClientExecutor getDefaultExecutor()
   {
      try
      {
         Class<?> clazz = Thread.currentThread().getContextClassLoader().loadClass(defaultExecutorClasss);
         return (ClientExecutor) clazz.newInstance();
      }
      catch (Exception e)
      {
         throw new RuntimeException(e);
      }
   }

あとはこの ApacheHttpClient4Executor の実装を見れば、httpClient に代表される httpComponents のクラスが山積みです。

httpComponent における HTTP コネクション管理

HTTP での通信を行う際は当然ながら HTTP コネクションを管理する必要があります。 httpComponent においては、この責務は ClientConnectionManager Interface の実装クラスが負うということになっています。具体的には、ClientConnectionManager

  • HTTP コネクションの作成
  • Keep Alive な HTTP コネクションの管理
  • 個々のスレッドに対してどの HTTP コネクションを使わせるのかの制御

を行うっていう Interface となり、その実装クラスとして、後述する BasicClientConnectionManagerPoolingClientConnectionManager なんてものが存在しています。

RestEasy のデフォルト

さて、RestEasy に戻りますが、何も意識しないで RestEasy を使用すると、ClientExecutor としては先述のように ApacheHttpClient4Executor が使用されます。 そして、このコンストラクタでは、以下のように httpClient として DefaultHttpClient が生成されます。

   public ApacheHttpClient4Executor()
   {
      this.httpClient = new DefaultHttpClient();
      this.createdHttpClient  = true;
      checkClientExceptionMapper();
   }

DefaultHttpClient は、HTTP コネクションを管理する ClientConnectionManager として、 BasicClientConnectionManager を使う実装になっています。 BasicClientConnectionManager は HTTP コネクションを 1 本だけ管理するという ConnectionManager であって、シングルスレッドのみで使われることを意図したものになっています。

これまでのまとめ

これまでの RestEasy の挙動を簡単にまとめると、

  1. ClientRequest を直接使用して HTTP 要求を行う際、(ApacheHttpClient4Executor が都度生成される結果として) 都度 httpClient が作成され、
  2. その中で、BasicClientConnectionManager が作られる

という振舞いになります。

実装1: BasicClientConnectionManager で close/release しない

たとえば以下のようなコードで 20 スレッドで HTTP 要求を行うと、HTTP コネクションが 20 本作成されるとともに、個々の HTTP コネクションが keep-alive のタイムアウト時間が経過するまで開放されない(5 秒間の間、20 本の ESTABLISHED な TCP コネクションが作成され、それが 5 秒後に CLOSE_WAIT に遷移し、当分の間残存する。

   public static void main(String... args) throws Exception {
        final int threadNum = 20;
        final String uri = "http://192.168.99.100:8080"; // Docker コンテナ上の Apache Listen ポート

        ExecutorService service = Executors.newFixedThreadPool(threadNum);
        for (int i = 0; i < threadNum; ++i) {
            service.submit(new GetRunnable(uri));
        }
        service.shutdown();

        // netstat 観測用の時間
        Thread.sleep(10000L);
    }

    public static class GetRunnable implements Runnable {

        private String uri;

        public GetRunnable(String uri) {
            this.uri = uri;
        }

        public void run() {
            try {
                new ClientRequest(uri).get();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

    }

実装2: PoolingClientConnectionManager で close/release しない

次に PoolingClientConnectionManager を使用してみる。この ConnectionManager は、その名前の通り、HTTP コネクションをプールとして管理する。 RestEasy が (正しくは httpComponent が)デフォルトで使用する BasicClientConnectionManager との違いは、BasicClientConnectionManager は個々の実行スレッドがそれぞれインスタンスを保持することを意図している一方で、PoolingClientConnectionManager はマルチスレッドで共有されることを(通常は)意図している点となる。 PoolingClientConnectionManager を使用するためには、次のように書けば良い。

   public static void main(String... args) throws Exception {
        final int threadNum = 20;
        final String uri = "http://192.168.99.100:8080";

        ClientConnectionManager connManager = new PoolingClientConnectionManager();
        HttpClient client = new DefaultHttpClient(connManager);

        ExecutorService service = Executors.newFixedThreadPool(threadNum);
        for (int i = 0; i < threadNum; ++i) {
            service.submit(new GetRunnable(uri, client));
        }

        service.shutdown();
    }

    public static class GetRunnable implements Runnable {

        private String uri;
        private HttpClient client;

        public GetRunnable(String uri, HttpClient client) {
            this.uri = uri;
            this.client = client;
        }

        public void run() {
            ClientExecutor executor = new ApacheHttpClient4Executor(client);
            try {
                new ClientRequest(uri, executor).get();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

    }

実際にこれを実行すると、非常に面白いことが起こる。 20 本のスレッドを同時実行しているはずなのに、Apache のアクセスログに記録されるのは 2 リクエストのみである。おい残りの 18 本はどこに行った。

192.168.99.1 - - [23/Apr/2016:13:03:52 +0000] "GET / HTTP/1.1" 200 12
192.168.99.1 - - [23/Apr/2016:13:03:52 +0000] "GET / HTTP/1.1" 200 12

また、この後で netstat を見ると、以下のように 2 本のコネクションが CLOSE_WAIT 状態のままとなっている。

tcp4       0      0  192.168.99.1.64268     192.168.99.100.8081    CLOSE_WAIT
tcp4       0      0  192.168.99.1.64267     192.168.99.100.8081    CLOSE_WAIT

種明かしをすると、以下の 2 つに起因して、残りの 18 スレッドは HTTP コネクション待ちの状態になっている。

  1. コネクションを適切に release していない
  2. PoolingClientConnectionManager のプール数の上限は、デフォルトだとトータルで 20 本、かつ、1 つの宛先ホスト毎に 2 本という制限がある

1 つ目の問題を解決するためには、ClientResponse#releaseConnection を呼び出せば良い。 releaseConnection は、HTTP コネクションをプールに返却するメソッドである。これにより HTTP コネクションが他のスレッドから使用できるようになるため、先ほどのような問題は解消される。

diff --git a/src/main/java/com/kiririmode/blog/http/client/ResteasyClient.java b/src/main/java/com/kiririmode/blog/http/client/ResteasyClient.java
index 34198f6..e54889a 100644
--- a/src/main/java/com/kiririmode/blog/http/client/ResteasyClient.java
+++ b/src/main/java/com/kiririmode/blog/http/client/ResteasyClient.java
@@ -9,6 +9,7 @@ import org.apache.http.impl.client.DefaultHttpClient;
 import org.apache.http.impl.conn.PoolingClientConnectionManager;
 import org.jboss.resteasy.client.ClientExecutor;
 import org.jboss.resteasy.client.ClientRequest;
+import org.jboss.resteasy.client.ClientResponse;
 import org.jboss.resteasy.client.core.executors.ApacheHttpClient4Executor;

 public class ResteasyClient {
@@ -41,10 +42,15 @@ public class ResteasyClient {

                public void run() {
                        ClientExecutor executor = new ApacheHttpClient4Executor(client);
+                       ClientResponse<?> response = null;
                        try {
-                               new ClientRequest(uri, executor).get();
+                               response = new ClientRequest(uri, executor).get();
                        } catch (Exception e) {
                                e.printStackTrace();
+                       } finally {
+                               if (response != null) {
+                                       response.releaseConnection();
+                               }
                        }
                }

2 つ目の問題に対しては、単純にプールの設定を変更してやれば良い。

diff --git a/src/main/java/com/kiririmode/blog/http/client/ResteasyClient.java b/src/main/java/com/kiririmode/blog/http/client/ResteasyClient.java
index e54889a..14ae85b 100644
--- a/src/main/java/com/kiririmode/blog/http/client/ResteasyClient.java
+++ b/src/main/java/com/kiririmode/blog/http/client/ResteasyClient.java
@@ -18,7 +18,9 @@ public class ResteasyClient {
                final int threadNum = 20;
                final String uri = "http://192.168.99.100:8081";

-               ClientConnectionManager connManager = new PoolingClientConnectionManager();
+               PoolingClientConnectionManager connManager = new PoolingClientConnectionManager();
+               connManager.setDefaultMaxPerRoute(10);
+               connManager.setMaxTotal(100);
                HttpClient client = new DefaultHttpClient(connManager);

                ExecutorService service = Executors.newFixedThreadPool(threadNum);

実装3: PoolingClientConnectionManager で close/release する

残りの問題は、CLOSE_WAIT が残り続ける問題なのだけれど、これは単純に ConnectionManager に対して HTTP コネクションの close を指示すれば良い。 これを行うのは ClientConnectionManager#shutdown メソッドである。 以下のように呼び出すことにより、TCP コネクションが CLOSE_WAIT 状態で残存することはなくなった。

diff --git a/src/main/java/com/kiririmode/blog/http/client/ResteasyClient.java b/src/main/java/com/kiririmode/blog/http/client/ResteasyClient.java
index 14811f9..b229cde 100644
--- a/src/main/java/com/kiririmode/blog/http/client/ResteasyClient.java
+++ b/src/main/java/com/kiririmode/blog/http/client/ResteasyClient.java
@@ -2,6 +2,7 @@ package com.kiririmode.blog.http.client;

 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;

 import org.apache.http.client.HttpClient;
 import org.apache.http.impl.client.DefaultHttpClient;
@@ -28,7 +29,9 @@ public class ResteasyClient {
                }

                service.shutdown();
-               Thread.sleep(10000L);
+               service.awaitTermination(10L, TimeUnit.SECONDS);
+
+               client.getConnectionManager().shutdown();
        }

        public static class GetRunnable implements Runnable {

パスワード等の秘匿情報をKeyStoreで保持する

DB 接続用のパスワードだったり、Basic/Digest 認証のときに使うパスワードだったり、世の中にはパスワードが溢れていて、もうパスワードを個別にして記憶しておくとか絶対にムリ、ムリムリムリムリかたつむりであるから、みんなどの Web サービスのアカウントにも共通のパスワードを使い回したりする。 パスワードを使い回していると発言すると、どこからともなくセキュリティおじさんが飛んできて握りこぶしでゲンコツをしていくので、ぼくたちはもうパスワードを使いまわしできなくなる。こうなるともはや IT の力に頼らざるをえなくて、iCloud とか 1Passsword とか、Chrome の Smart Lock for Passwords とかで、暗号化したパスワードを保存しておいて、使いたいときに使えるようにする、みたいなソリューションを採る。

こういうのはシステムにおいても同じであって、1 つのシステムを作ると、たくさんのパスワードを使う状況に陥る。これを平文で保持しておくと、どこからともなくセキュリティおじさんが飛んできて頬を平手で殴っていくので、ぼくたちはパスワードを平文で保存できなくなる。こうなるともはや IT の力に頼らざるを得なくなる。

パスワード管理をどうしようか

パスワード管理をどうしようと考えると、やっぱしパスワードを暗号化してどっかに持たせておくしかなくて、Java だとそういうのに KeyStore がある。 KeyStore はそもそも暗号化に使う鍵とかを保持するものだけれど、一種の KVS としても使うことができて、一応その Value もなんちゃって暗号化される。 というわけで、パスワードもこの KeyStore に放りこんでしまおうという前提において、その実装を考えてみる。

KeyStore の種類をどうするか

KeyStore にはいくつか種類がある。Java の世界で(現状)の標準としては JKS という形式のものがあり、他にも JCEKS とか PKCS12PKCS11 とかある。 また、KeyStore に投入できるものとしては

  • 秘密鍵
  • 共通鍵
  • 証明書

が定義されているのだけれど、たとえば JKS には共通鍵が保存できなかったり、何でもかんでも互換性があるわけではない。

今回、パスワードを暗号化して KeyStore に放り込もうとすると、任意の文字列(暗号文) を KeyStore に放り込む必要があり、これを実現できるのはこのうちの共通鍵のみとなる。共通鍵に対応した KeyStore の形式は、というと、JCEKSPKCS12PKCS11 あたりが候補になる (JKS は共通鍵には対応していない)。

どうやって任意の文字列を KeyStore に投入するか

KeyStore に投入できるオブジェクトというのは、基本的には KeyStore.Entry Interface の実装クラスのみであり、上記に示した投入できるものとの対応関係は以下のようになる。

  • 秘密鍵: KeyStore.PrivateKeyEntry
  • 共通鍵: KeyStore.SecretKeyEntry
  • 証明書: KeyStore.TrustedCertificateEntry

このため、今回は「パスワードを暗号化した任意の文字列」 を KeyStore.SecretKeyEntry として構築できれば良い。

パスワードの暗号化については非常に単純に記述でき、以下に示したような Java プログラムで暗号化できる。

    Cipher cipher = Cipher.getInstance(algorithm);
    cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(hexEncodedKey), getIv(hexEncodedIv));
    byte[] encryptedSecret = cipher.doFinal(secret.getBytes(StandardCharsets.UTF_8));

なので、後は任意の暗号化文 (byte 配列) を KeyStore.SecretKeyEntry として構築するところを考える。

KeyKeySpec

実は Java における「鍵」には大きく 2 つあり、Key (鍵) と KeySpec (鍵仕様) である。

この 2 つの概念の違いは以下のページに記載があるが、これを一部運用とすると以下のとおり。

KeyとKeySpecはあまり変わらない表現に見えますが、実はこの2つには明確な違いがあります。 Keyインタフェースからはその鍵のデータに直接アクセスすることができません。このためKeyは不透明な鍵表現と 呼ばれ、それに対してKeySpecは鍵のデータに直接アクセスできるため透明な鍵表現と呼ばれます。 またKeyを鍵と呼ぶのに対して、KeySpecは鍵仕様と呼ばれます。

要するに具体的な値を持つのは KeySpec である。そして共通鍵における KeySpecSecretKeySpec であるので、これを構築すれば良い。さらにいえば、SecretKeySpecKey でもあり、KeySpec でもある (両者は Interface であり、SecretKeySpec は両 Interface を実装している)ので、両者を変換する必要はない。 あとは、KeyStore.SecretKeyEntry のコンストラクタが以下のように SecretKey を引数に取るメソッドシグニチャであるので、これを使って SecretKeyEntry を構築し、それを KeyStore に登録すれば良い。

public KeyStore.SecretKeyEntry(SecretKey secretKey)

具体的なコードは次のようになる。これで KeyStore に任意の暗号文が登録できた。

   KeyStore ks = getKeyStore(keyStoreUrl, keyStorePassword, keyStoreType);

    Cipher cipher = Cipher.getInstance(algorithm);
    cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(hexEncodedKey), getIv(hexEncodedIv));
    byte[] encryptedSecret = cipher.doFinal(secret.getBytes(StandardCharsets.UTF_8));

    SecretKeySpec keySpec = new SecretKeySpec(encryptedSecret, "AES");
    KeyStore.SecretKeyEntry entry = new SecretKeyEntry(keySpec);
    ks.setEntry(alias, entry, new KeyStore.PasswordProtection(keyStorePassword.toCharArray()));

    try (FileOutputStream fos = new FileOutputStream(keyStoreUrl)) {
        ks.store(fos, keyStorePassword.toCharArray());
    }

これができれば、KeyStore からパスワードを取り出すのは簡単で、上記の対称形になる。

   KeyStore ks = getKeyStore(keyStoreUrl, keyStorePassword, keyStoreType);
    KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry) ks.getEntry(alias,

    new KeyStore.PasswordProtection(keyStorePassword.toCharArray()));
    byte[] encrypted = secretKeyEntry.getSecretKey().getEncoded();

    Cipher cipher = Cipher.getInstance(algorithm);
    cipher.init(Cipher.DECRYPT_MODE, getSecretKey(hexEncodedKey), getIv(hexEncodedIv));
    byte[] decrypted = cipher.doFinal(encrypted);

    return new String(decrypted, StandardCharsets.UTF_8);

とりあえずの実装は、https://github.com/kiririmode/vault に。

コマンドラインで簡単にAES暗号化、または Java での AES 暗号化

Java で AES 暗号化とかやってて、コマンドラインで簡単に試せれば良いのになぁとか思ってたら、openssl 使ったら余裕で暗号化できることに気付いた。

例えば、AES/CBC の 128 bit 鍵長で暗号化したい場合は、以下のようにすれば良い。 -e は暗号化、-d は復号化を示している。

$ echo "kiririmode" | openssl aes-128-cbc -e -base64
enter aes-128-cbc encryption password:
Verifying - enter aes-128-cbc encryption password:
U2FsdGVkX190LTIvjNslBh78S+fbl+Lj8akdU/I9qGY=

$ echo "U2FsdGVkX190LTIvjNslBh78S+fbl+Lj8akdU/I9qGY=" | openssl aes-128-cbc -d -base64
enter aes-128-cbc decryption password:
kiririmode

共通鍵と初期ベクトルはどこにいった?

共通鍵も、(CBC にも関わらず)初期ベクトルを指定していないのだけれど、それは openssl が自動生成してくれる。この自動生成については途中でパスワードを尋ねられているのがミソで、実は openssl では、入力したパスワードから共通鍵と初期ベクトルを自動生成している。 このあたりの解説は、以下のサイトがくわしい。

実際に使われた共通鍵と初期ベクトルについては、-p オプションをつければ分かる。 なお、パスワードをインタラクティブに聞かれるのが煩わしい場合は -pass オプションで指定が可能。

$ echo "kiririmode" | openssl aes-128-cbc -e -base64 -p -pass pass:password
salt=ABD594EB1B826A18
key=1C99D62AFD668EDF30EE2E04385B6E29
iv =FD086E25447B49E23D48FDB3A75CF09C
U2FsdGVkX1+r1ZTrG4JqGInbs//CpkHEzmqvmlsLGTE=

共通鍵と初期ベクトルを明示的に指定する

当然ながら共通鍵と初期ベクトルは明示的に指定することが可能で、-K-iv オプションで、それぞれ共通鍵と初期ベクトルを指定する。 指定フォーマットは HEX encoded なので、双方ともに 16 進数で 32 桁 (128 bit) を指定すれば良い。 この場合、共通鍵、初期ベクトルは指定済なので、パスワードを指定する必要はない。

$ echo "kiririmode" | openssl aes-128-cbc -e -base64 -iv 1234567890ABCDEF1234567890ABCDEF -K 12345678901234567890123456789012 | openssl aes-128-cbc -d -base64 -iv 1234567890ABCDEF1234567890ABCDEF -K 12345678901234567890123456789012
kiririmode

パディング方式は?

openssl では、Padding 方式は PKCS#5 を使用する。このため、同様に PKCS#5 をサポートする実行系では openssl の暗号化結果を復号化できるし、また逆も然り。 たとえば、以下のプログラムは、openssl の暗号化結果を復号化できるし、また、その逆も当然可能。

package com.kiririmode.blog;

import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Base64;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;

public class AESEncryptor {

    private static String ALGORITHM = "AES/CBC/PKCS5Padding";

    public String encrypt(String hexEncodedKey, String hexEncodedIv, String plainText) throws GeneralSecurityException {

        Cipher cipher = Cipher.getInstance(ALGORITHM);
        cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(hexEncodedKey), getIv(hexEncodedIv));
        byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));

        return Base64.getEncoder().encodeToString(encrypted);
    }

    public String decrypt(String hexEncodedKey, String hexEncodedIv, String cipheredString)
            throws GeneralSecurityException {
        byte[] ciphered = Base64.getDecoder().decode(cipheredString);

        Cipher cipher = Cipher.getInstance(ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, getSecretKey(hexEncodedKey), getIv(hexEncodedIv));
        byte[] plainBytes = cipher.doFinal(ciphered);

        return new String(plainBytes, StandardCharsets.UTF_8);

    }

    private Key getSecretKey(String hexEncodedKey) {
        return new SecretKeySpec(DatatypeConverter.parseHexBinary(hexEncodedKey), "AES");
    }

    private AlgorithmParameterSpec getIv(String hexEncodedIv) {
        return new IvParameterSpec(DatatypeConverter.parseHexBinary(hexEncodedIv));
    }
}

その他に使用できる暗号は?

openssl enc -h あたりで見ることができるよ!