AWS SDK for Java + KMS で S3 暗号化

備忘録。前回の続き。

AWS SDK for Java で署名付き URL 生成 - kntmr-blog

Key Management Service (KMS) と AWS SDK for Java で、クライアントサイドで暗号化して S3 にアップロードする。

キー作成 (CMS)

事前に Key Management Service > カスタマー管理型のキー でキーを作成する。今回はダウンロードして復号したいので、キーのタイプには 対称 を選択する。

AWS Encryption SDK

AWS Encryption SDK for Java - AWS Encryption SDK

暗号化に必要なライブラリを追加。これがないと AmazonS3EncryptionV2 を初期化する際に実行時エラーが発生する。

<dependency>
    <groupId>com.amazonaws</groupId>
    <artifactId>aws-encryption-sdk-java</artifactId>
    <version>2.0.0</version>
</dependency>

暗号化&アップロード

パターン1

AmazonS3EncryptionV2 s3Encryption = AmazonS3EncryptionClientV2Builder.standard()
        .withCredentials(new ProfileCredentialsProvider())
        .withCryptoConfiguration(new CryptoConfigurationV2()
                .withCryptoMode(CryptoMode.StrictAuthenticatedEncryption))
        .withEncryptionMaterialsProvider(new KMSEncryptionMaterialsProvider(keyId))
        .build();

try {
    s3Encryption.putObject(bucketName, objectKey, file);
} finally {
    s3Encryption.shutdown();
}

パターン2

SDK から KMS のキーを作成してアップロードするパターン。ついでにキーの削除スケジュールをリクエストする。ScheduleKeyDeletionRequest#withPendingWindowInDays にはキーが削除可能になるまでの待機日数を指定する。(7〜30)

AWSKMS kmsClient = AWSKMSClientBuilder.standard()
        .withCredentials(new ProfileCredentialsProvider())
        .build();

CreateKeyRequest createKeyRequest = new CreateKeyRequest();
CreateKeyResult createKeyResult = kmsClient.createKey(createKeyRequest);
String keyId = createKeyResult.getKeyMetadata().getKeyId();

AmazonS3EncryptionV2 s3Encryption = AmazonS3EncryptionClientV2Builder.standard()
        .withCredentials(new ProfileCredentialsProvider())
        .withCryptoConfiguration(new CryptoConfigurationV2()
                .withCryptoMode(CryptoMode.StrictAuthenticatedEncryption))
        .withEncryptionMaterialsProvider(new KMSEncryptionMaterialsProvider(keyId))
        .build();

try {
    s3Encryption.putObject(bucketName, objectKey, file);

    ScheduleKeyDeletionRequest scheduleKeyDeletionRequest = new ScheduleKeyDeletionRequest()
            .withKeyId(keyId)
            .withPendingWindowInDays(7);
    kmsClient.scheduleKeyDeletion(scheduleKeyDeletionRequest);
} finally {
    s3Encryption.shutdown();
    kmsClient.shutdown();
}

ダウンロード&復号

AmazonS3EncryptionV2 s3Encryption = AmazonS3EncryptionClientV2Builder.standard()
        .withCredentials(new ProfileCredentialsProvider())
        .withCryptoConfiguration(new CryptoConfigurationV2()
                .withCryptoMode(CryptoMode.StrictAuthenticatedEncryption))
        .withEncryptionMaterialsProvider(new KMSEncryptionMaterialsProvider(keyId))
        .build();

try {
    GetObjectRequest getObjectRequest = new GetObjectRequest(bucketName, objectKey);
    s3Encryption.getObject(getObjectRequest, file);
} finally {
    s3Encryption.shutdown();
}

その他

暗号化したときの keyId 以外を指定した場合、AmazonS3EncryptionV2#getObject でエラーが発生する。(status code 400)

com.amazonaws.services.kms.model.IncorrectKeyException: The key ID in the request does not identify a CMK that can perform this operation.

暗号化してアップロードしたファイルは AmazonS3#getObject でもダウンロードできるが、暗号化されているため開けない。マネジメントコンソールからダウンロードした場合も同様に開けない。

暗号化したファイルのコピーや削除では keyId の指定は不要で、AmazonS3#copyObject or AmazonS3.deleteObject が使える。もちろん、暗号化したままコピーされる。

AWS SDK for Java で S3 署名付き URL 生成

備忘録。S3 の 署名付き URL を AWS SDK for Java で生成する。AWS の設定周りについては正しいかどうかは自信がない...。

<!-- https://mvnrepository.com/artifact/com.amazonaws/aws-java-sdk-s3 -->
<dependency>
    <groupId>com.amazonaws</groupId>
    <artifactId>aws-java-sdk-s3</artifactId>
    <version>1.11.1034</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.amazonaws/aws-java-sdk-sts -->
<dependency>
    <groupId>com.amazonaws</groupId>
    <artifactId>aws-java-sdk-sts</artifactId>
    <version>1.11.1034</version>
</dependency>

パターン1

IAM ユーザーの認証情報 (accessKey, secretKey) を利用して生成するパターン。

AmazonS3 amazonS3 = AmazonS3ClientBuilder.standard()
        .withCredentials(new ProfileCredentialsProvider())
        .build();

Date expiration = new Date();
expiration.setTime(Instant.now().toEpochMilli() + 1000 * 60 * 5); // 有効期限5分
GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(bucketName, objectKey)
        .withMethod(HttpMethod.GET) // GET のみ許可する
        .withExpiration(expiration);

URL presignedUrl = amazonS3.generatePresignedUrl(request);

パターン2

AWS STS で一時的なセキュリティ認証情報を取得して生成するパターン。事前に AmazonS3ReadOnlyAccess をアタッチしたロールを作成する。また、「信頼関係の編集」で sts:RoleSessionName を追加する。

... (略)
"Condition": {
  "StringLike": {
    "sts:RoleSessionName": "${aws:username}"
  }
}
AWSSecurityTokenService stsClient = AWSSecurityTokenServiceClientBuilder.standard()
        .withCredentials(new ProfileCredentialsProvider())
        .build();

AssumeRoleRequest roleRequest = new AssumeRoleRequest()
        .withRoleArn(roleArn)
        .withRoleSessionName(roleSessionName);
AssumeRoleResult roleResult = stsClient.assumeRole(roleRequest);
Credentials sessionCredentials = roleResult.getCredentials();

BasicSessionCredentials awsCredentials = new BasicSessionCredentials(
        sessionCredentials.getAccessKeyId(),
        sessionCredentials.getSecretAccessKey(),
        sessionCredentials.getSessionToken());

AmazonS3 amazonS3 = AmazonS3ClientBuilder.standard()
        .withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
        .build();

Date expiration = new Date();
expiration.setTime(Instant.now().toEpochMilli() + 1000 * 60 * 5); // 有効期限5分
GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(bucketName, objectKey)
        .withMethod(HttpMethod.GET) // GET のみ許可する
        .withExpiration(expiration);

URL presignedUrl = amazonS3.generatePresignedUrl(request);

その他

パターン2では URL に X-Amz-Security-Token=... パラメータが付く。このパラメータで一時的なセキュリティ認証情報を渡す。

ちなみに、マネジメントコンソールの「開く」でも署名付き URL は生成できる。有効期限は5分。(X-Amz-Expires=300)

JJUGナイトセミナー「オブジェクト指向プログラミング入門」に行ってきた #jjug

JJUGナイトセミナー「オブジェクト指向プログラミング入門」に参加しました。オンライン開催。簡単に所感をまとめます。

jjug.doorkeeper.jp

所感

Software Design 2021年3月号 の特集を執筆された3名によるセッション。設計やオブジェクト指向についてどう考えているかを聴けて面白かったです。

増田さんの話は自身の経験をベースにしてるんだろうけど、こういう話を体系的に整理して説明できるところがすごい。

あと、建築の設計はぜんぜん詳しくないけど、システム開発の設計は建築の設計よりもっと上流のフェーズにあるようなものな気がして、対比して語るのはちょっと違和感あるかなって。

小クラス主義については、全体的な設計の見通しを立てるために (分割する前提で) 最初は大きく作ることはあるかもしれないけど、難しいところ。

個人的には、設計は全体を俯瞰するプロセスで、プログラミングはもっとスコープが小さい話かなと思っています。で、犬猫とか車とエンジンみたいな例え話はプログラミング言語の文法を学ぶときに有用かなと思っていて、設計を身に付けるならシステムを図とか文章で書き起こして全体を理解するところから始めるとよさそうかなと思いました。たぶん。

以下、メモから抜粋。

なぜ設計を学ぶ必要があるのか

  • 設計はシステム具象化の準備
  • システムを抽象的に捉える
  • 抽象, 捨象
  • 見積もりや設計は過去の経験の抽象化
  • プログラミング=具象 と 設計=抽象 を往復する
  • 抽象化能力を経験以外から学べるか
  • 設計を学問として学ぶ

オブジェクト指向と関数型を組み合わせる

  • 関数型とオブジェクト指向は直交する
  • 関数型の利点
    • オブジェクトの不変性
    • スレッドセーフ
  • オブジェクト指向の利点
    • メソッドがクラスに所属する
    • 関係する演算はクラスの public API を見ればわかる
  • 不変クラスの設計
    • equals(), hashCode(), toString()
    • Record

クラス設計本格入門

  • オブジェクト指向プログラミングはクラス設計
  • クラス設計はプログラムの分割
  • クラスはロジックとデータの集約
  • 小クラス主義
    • 小さく分割, 変更の影響の局所化, 部品としての再利用
  • 不変
  • 分割と統合の労力
  • リファクタリング (分割の改善) を少しずつ積み立てる
  • 分割したクラスを組み合わせるのは容易
  • 分割
    • メソッド/クラスの抽出 & 名前付け
    • パッケージ/サブパッケージ & 名前付け
  • パッケージ/クラスの名前で分割意図の表現を明確にする
  • クラス/パッケージの凝集度を上げる
  • ビジネスアクションの表現, ビジネスルールの表現
    • ビジネスルールのクラスに void はありえない
  • 対象領域(ドメイン)の関心事で分割する
    • 周りにある関連語彙を増やして知識を広げる
    • 基本の言葉のまわりにはさまざまな決め事 (ビジネスルール) がある
  • 範囲や区分のカプセル化
  • コレクション操作のカプセル化

座談会

  • 不変性を実現するにはハードウェア進化の恩恵が大きい
  • サブタイプ, サブクラス の違い
  • immutable な Collection ライブラリ欲しい
  • String クラスは役割持ちすぎ
  • File IO は役割わけすぎ
    • InputStream, Reader/Writer とか
    • Files でわりとラクになった
  • Stream は微妙に物足りない
  • static or インスタンスメソッド
    • ファクトリーメソッド
    • シグネチャが違うコンストラクタを並べたくない
    • Builder にしたり
    • sort は Collection が持つべき
  • static にすると mock にできない
    • DBUtil, FileUtil, ...
  • 継承可否を言語仕様で提供するのは?
    • 継承できないのがデフォルトでよい
    • 実装継承は使わない方がよい
    • テストしやすいならデフォルト private でよい
    • package private はバランスよい

timestamp 型のカラムに integer 型のカラムの値の加算して date 型で比較する

備忘録。

例えば、created_at カラムに days カラムの値を日数として加算して、その結果を日付で比較するようなケース。使い道があるかは分からないけど。

以下は PostgreSQL の場合。

SELECT * FROM table t
WHERE CAST(t.created_at + CAST(t.days || 'days' AS INTERVAL) AS date) = '2021-06-02'

Future Tech Night #10 に行ってきた #future_tech_night

Future Tech Night #10 に参加しました。オンライン開催。簡単に所感をまとめます。

future.connpass.com

所感

前半は Java 8 から Java 16 で追加された API のおさらいみたいな感じでした。Record の機能は Lombok でまかなえるっていう意見が TL で流れてたけど、標準機能として提供されるところに価値があると思う。

後半は Tomcat の話。Tomcat のコミッターで 詳解 Tomcat の著者。マニアックというだけあって、知らない内容もあってとても参考になりました。

今週末は JJUG CCC かー。

以下、メモから抜粋。

Javaレガシー言語からモダンな言語へ。どこまで知ってる?Java最新事情!

  • Java 8 から Java 16 の API おさらい
  • ラムダ式
  • Stream API
  • Optional
  • Date and Time API
  • var
  • HttpClient (HTTP/2)
  • Switch 式
  • テキストブロック
  • Record
  • instanceOf
  • Sealed Class

Future では 14 を使っているところが多い。16 へのバージョンアップもあるかも。バージョンの追随はコストとの兼ね合い。deprecated な API の調査が大変。

Tomcatコミッタがお送りするちょっとマニアックなコンフィグレーション10選

  • 設定値を外部リソースから読み込む
  • ポートオフセット
  • JSON Error Report
    • デフォルトの HTML ではなく JSON で返す
  • StuckThreadDetectionValve
    • 閾値以上、スタックしたスレッドの stacktrace が出力される
    • スレッドダンプのようなロック情報は見れない
  • レガシーアプリケーションベース
  • 静的クラスタメンバーシップ
    • クラスタリングのメンバーシップはデフォルトではマルチキャスト
    • StaticMembershipService
      • IPアドレスをキャッシュしているので要注意 (今後修正予定)
    • CloudMembershipService
      • k8s 向け
      • Pod に対して Cloud メンバーシップを設定する
      • 公式ドキュメントには記載がない (GitHub を見よう)
  • 組み込み Tomcat のコンフィグソース
    • new Tomcat() したあとの設定はどうするか
    • ConfigurationSource
    • CatalinaBaseConfigurationSource
      • server.xml をパースして設定してくれる
    • -generateCode 起動オプション
  • 多言語対応
    • ログは環境に合わせた言語で出力される
    • POEditor
    • 変なのがあったらコントリビュートしましょう
  • Tomcat 10 以降のバージョン体系
  • TomcatJava EE のサブセットをサポートしている
  • Tomcat 10 は Jakarta EE 9 をサポートする
  • Jakarta EE と併せて Java EE 8 は長めにサポートされそう
  • 自分のアプリケーションをどのバージョンの Tomcat で動かすか
  • Apache/Tomcat の通信は HTTP にした方がよい
    • AJP脆弱性があるため今後はやめる方向になりそう

macOS をアップデートしたあと Command Line Tools のインストールが失敗する

ちょっとハマったので備忘録。brew install でパッケージをインストールしようとしたらこんなエラーが出力された。

Error: The following formula cannot be installed from bottle and must be built from source.
Install the Command Line Tools:
  xcode-select --install

先日、macOS をアップデートした影響で Command Line Tools がなくなったっぽい。言われた通り、xcode-select --install を実行。

$ xcode-select --install

インストーラーが起動してそこそこ時間がかかった挙げ句、「ソフトウェアをインストールできません」と表示される。代わりに Apple Developers から dmg をダウンロードして実行してみたが同様。(こっちもそこそこ時間がかかる...)

解決策

OS アップデート前の Command Line Tools の旧ディレクトリが残っているとインストールが失敗するっぽい。(/Library/Developer/CommandLineTools)

$ sudo rm -rf /Library/Developer/CommandLineTools # 消すのが心配ならリネームでもOK
$ sudo xcode-select --install

現場からは以上です。