145 lines
5.1 KiB
Kotlin
145 lines
5.1 KiB
Kotlin
package jp.juggler.crypt
|
|
|
|
import jp.juggler.util.data.encodeUTF8
|
|
import java.io.ByteArrayOutputStream
|
|
import java.security.Provider
|
|
import javax.crypto.Cipher
|
|
import javax.crypto.spec.GCMParameterSpec
|
|
import javax.crypto.spec.SecretKeySpec
|
|
|
|
/**
|
|
* Content-Encoding: aesgcm のデコード。
|
|
* 中間状態のテストがしたいので状態をもつクラスとして実装した
|
|
*/
|
|
class AesGcmDecoder(
|
|
// receiver private key in X509 format
|
|
receiverPrivateBytes: ByteArray,
|
|
// receiver public key in 65bytes X9.62 uncompressed format, created at subscription
|
|
private val receiverPublicBytes: ByteArray,
|
|
// auth secret, created at subscription
|
|
authSecret: ByteArray,
|
|
// sender public key in 65bytes X9.62 uncompressed format
|
|
private val senderPublicBytes: ByteArray,
|
|
// salt in HTTP header
|
|
saltBytes: ByteArray,
|
|
// provider for security functions
|
|
private val provider: Provider = defaultSecurityProvider,
|
|
) {
|
|
companion object {
|
|
private const val b0 = 0.toByte()
|
|
|
|
private val infoCeAuthZ = "Content-Encoding: auth\u0000".encodeUTF8().toByteRange()
|
|
private val prefixCeAesGcmZP256Z = "Content-Encoding: aesgcm\u0000P-256\u0000".encodeUTF8()
|
|
private val prefixCeNonceZP256Z = "Content-Encoding: nonce\u0000P-256\u0000".encodeUTF8()
|
|
|
|
fun ByteArrayOutputStream.writeLengthAndBytes(b: ByteArray) {
|
|
val len = b.size
|
|
write(len.shr(8).and(255))
|
|
write(len.and(255))
|
|
write(b)
|
|
}
|
|
}
|
|
|
|
private val salt = saltBytes.toByteRange()
|
|
|
|
val auth = authSecret.toByteRange()
|
|
|
|
val sharedKeyBytes = provider.sharedKeyBytes(
|
|
receiverPrivateBytes = receiverPrivateBytes,
|
|
senderPublicBytes = senderPublicBytes,
|
|
)
|
|
|
|
//
|
|
var ikm = ByteRange.empty
|
|
|
|
@Suppress("MemberVisibilityCanBePrivate")
|
|
var prk = ByteRange.empty
|
|
var key = ByteRange.empty
|
|
var nonce = ByteRange.empty
|
|
|
|
// The start index for each element within the buffer is:
|
|
// value | length | start |
|
|
// -----------------------------------------
|
|
// 'Content-Encoding: '| 18 | 0 |
|
|
// type | len | 18 |
|
|
// nul byte | 1 | 18 + len |
|
|
// 'P-256' | 5 | 19 + len |
|
|
// nul byte | 1 | 24 + len |
|
|
// client key length | 2 | 25 + len |
|
|
// client key | 65 | 27 + len |
|
|
// server key length | 2 | 92 + len |
|
|
// server key | 65 | 94 + len |
|
|
private fun createInfo(
|
|
prefix: ByteArray,
|
|
clientPublicKey: ByteArray, // 65 byte
|
|
serverPublicKey: ByteArray, // 65 byte
|
|
) = ByteArrayOutputStream(120).apply {
|
|
write(prefix)
|
|
// For the purposes of push encryption the length of the keys will always be 65 bytes.
|
|
writeLengthAndBytes(clientPublicKey)
|
|
writeLengthAndBytes(serverPublicKey)
|
|
}.toByteRange()
|
|
|
|
fun deriveKey() {
|
|
// input key material の導出はWebPushの仕様に依存する
|
|
ikm = hmacSha256Plus1(
|
|
hmacSha256(auth, sharedKeyBytes).toByteRange(),
|
|
infoCeAuthZ
|
|
).toByteRange(end = 32)
|
|
|
|
// ikm, salt から prk, key, nonce を導出する
|
|
|
|
prk = hmacSha256(salt, ikm).toByteRange()
|
|
|
|
key = hmacSha256Plus1(
|
|
prk,
|
|
createInfo(
|
|
prefix = prefixCeAesGcmZP256Z,
|
|
clientPublicKey = receiverPublicBytes,
|
|
serverPublicKey = senderPublicBytes,
|
|
)
|
|
).toByteRange(end = 16)
|
|
|
|
nonce = hmacSha256Plus1(
|
|
prk,
|
|
createInfo(
|
|
prefix = prefixCeNonceZP256Z,
|
|
clientPublicKey = receiverPublicBytes,
|
|
serverPublicKey = senderPublicBytes,
|
|
)
|
|
).toByteRange(end = 12)
|
|
}
|
|
|
|
fun decode(src: ByteRange): ByteRange {
|
|
|
|
val cip = Cipher.getInstance("AES/GCM/NoPadding", provider)
|
|
cip.init(
|
|
Cipher.DECRYPT_MODE,
|
|
SecretKeySpec(key.ba, key.start, key.size, "AES"),
|
|
GCMParameterSpec(
|
|
GCM_TAG_BITS,
|
|
nonce.ba, nonce.start, nonce.size
|
|
),
|
|
)
|
|
var dst = cip.doFinal(src.ba, src.start, src.size).toByteRange()
|
|
|
|
// 多分不要だと思うが、
|
|
// https://greenbytes.de/tech/webdav/draft-ietf-httpbis-encryption-encoding-02.html
|
|
// にあるレコード先頭のパディングを除去する
|
|
// テキストメッセージなら nul文字が含まれないのでヒットしないはず
|
|
if (dst.size >= 3 && dst[2] == b0) {
|
|
val reader = dst.byteRangeReader()
|
|
val padLen = 2 + reader.readUInt16()
|
|
reader.skip(padLen)
|
|
dst = reader.remainBytes()
|
|
}
|
|
|
|
// 先頭の空白を除去する
|
|
var i = 0
|
|
while (i < dst.size && dst[i] in 0..32) ++i
|
|
if (i > 0) dst = dst.subRange(start = i)
|
|
|
|
return dst
|
|
}
|
|
}
|