196 lines
6.8 KiB
Kotlin
196 lines
6.8 KiB
Kotlin
package jp.juggler.crypt
|
|
|
|
import org.bouncycastle.jce.ECNamedCurveTable
|
|
import org.bouncycastle.jce.ECPointUtil
|
|
import org.bouncycastle.jce.spec.ECNamedCurveSpec
|
|
import org.conscrypt.Conscrypt
|
|
import java.math.BigInteger
|
|
import java.security.*
|
|
import java.security.interfaces.ECPrivateKey
|
|
import java.security.interfaces.ECPublicKey
|
|
import java.security.spec.ECGenParameterSpec
|
|
import java.security.spec.ECPrivateKeySpec
|
|
import java.security.spec.ECPublicKeySpec
|
|
import java.security.spec.PKCS8EncodedKeySpec
|
|
import javax.crypto.KeyAgreement
|
|
import javax.crypto.Mac
|
|
import javax.crypto.spec.SecretKeySpec
|
|
|
|
val defaultSecurityProvider by lazy {
|
|
Conscrypt.newProvider()!!.also { Security.addProvider(it) }
|
|
}
|
|
|
|
const val DEFAULT_CURVE_NAME = "secp256r1"
|
|
|
|
const val GCM_TAG_BITS = 16 * 8
|
|
|
|
// HKDFのExpandの末尾に付与する
|
|
private val hkdfTrailBytes = ByteArray(1) { 1 }.toByteRange()
|
|
|
|
// k=v;k=v;... を解釈する
|
|
fun String.parseSemicolon() = split(";").map { pair ->
|
|
pair.split("=", limit = 2).map { it.trim() }
|
|
}.mapNotNull {
|
|
when {
|
|
it.isEmpty() -> null
|
|
else -> it[0] to it.elementAtOrNull(1)
|
|
}
|
|
}.toMap()
|
|
|
|
// ECPrivateKey のS値を32バイトの配列にコピーする
|
|
// BigInteger.toByteArrayは可変長なので、長さの調節を行う
|
|
fun encodePrivateKeyRaw(key: ECPrivateKey): ByteArray {
|
|
val srcBytes = key.s.toByteArray()
|
|
return when {
|
|
srcBytes.size == 32 -> srcBytes
|
|
// 32バイト以内なら先頭にゼロを詰めて返す
|
|
srcBytes.size < 32 -> ByteArray(32).also {
|
|
System.arraycopy(
|
|
srcBytes, 0,
|
|
it, it.size - srcBytes.size,
|
|
srcBytes.size
|
|
)
|
|
}
|
|
// ビッグエンディアンの先頭に符号ビットが付与されるので、32バイトに収まらない場合がある
|
|
// 末尾32バイト分を返す
|
|
else -> ByteArray(32).also {
|
|
System.arraycopy(
|
|
srcBytes, srcBytes.size - it.size,
|
|
it, 0,
|
|
it.size
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* WebPushのp256dh、つまり公開鍵を X9.62 uncompressed format で符号化したバイト列を作る。
|
|
* - 出力の長さは65バイト
|
|
*/
|
|
fun encodeP256Dh(src: ECPublicKey): ByteArray = src.run {
|
|
val bitsInByte = 8
|
|
val keySizeBytes = (params.order.bitLength() + bitsInByte - 1) / bitsInByte
|
|
return ByteArray(1 + 2 * keySizeBytes).also { dst ->
|
|
var offset = 0
|
|
dst[offset++] = 0x04
|
|
w.affineX.toByteArray().let { x ->
|
|
when {
|
|
x.size <= keySizeBytes ->
|
|
System.arraycopy(
|
|
x, 0,
|
|
dst, offset + keySizeBytes - x.size,
|
|
x.size
|
|
)
|
|
|
|
x.size == keySizeBytes + 1 && x[0].toInt() == 0 ->
|
|
System.arraycopy(
|
|
x, 1,
|
|
dst, offset,
|
|
keySizeBytes
|
|
)
|
|
|
|
else -> error("x value is too large")
|
|
}
|
|
}
|
|
offset += keySizeBytes
|
|
w.affineY.toByteArray().let { y ->
|
|
when {
|
|
y.size <= keySizeBytes -> System.arraycopy(
|
|
y, 0,
|
|
dst, offset + keySizeBytes - y.size,
|
|
y.size
|
|
)
|
|
|
|
y.size == keySizeBytes + 1 && y[0].toInt() == 0 -> System.arraycopy(
|
|
y, 1,
|
|
dst, offset,
|
|
keySizeBytes
|
|
)
|
|
else -> error("y value is too large")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// JavaScriptのcreateECDHがエンコードした秘密鍵をデコードする
|
|
// https://github.com/nodejs/node/blob/main/lib/internal/crypto/diffiehellman.js#L232
|
|
// https://github.com/nodejs/node/blob/main/src/crypto/crypto_ec.cc#L265
|
|
fun Provider.decodePrivateKeyRaw(srcBytes: ByteArray): ECPrivateKey {
|
|
// 符号拡張が起きないように先頭に0を追加する
|
|
val newBytes = ByteArray(srcBytes.size + 1).also {
|
|
System.arraycopy(
|
|
srcBytes, 0,
|
|
it, it.size - srcBytes.size,
|
|
srcBytes.size
|
|
)
|
|
}
|
|
|
|
val s = BigInteger(newBytes)
|
|
|
|
// テキトーに鍵を作る
|
|
val keyPair = generateKeyPair()
|
|
// params部分を取り出す
|
|
val ecParameterSpec = (keyPair.private as ECPrivateKey).params
|
|
// s値を指定して鍵を作る
|
|
val privateKeySpec = ECPrivateKeySpec(s, ecParameterSpec)
|
|
return KeyFactory.getInstance("EC", this)
|
|
.generatePrivate(privateKeySpec) as ECPrivateKey
|
|
}
|
|
|
|
/**
|
|
* ECの鍵ペアを作成する
|
|
*/
|
|
fun Provider.generateKeyPair(curveName: String = DEFAULT_CURVE_NAME): KeyPair =
|
|
KeyPairGenerator.getInstance("EC", this).apply {
|
|
@Suppress("SpellCheckingInspection")
|
|
initialize(ECGenParameterSpec(curveName))
|
|
}.genKeyPair() ?: error("genKeyPair returns null")
|
|
|
|
fun Mac.update(br: ByteRange) = update(br.ba, br.start, br.size)
|
|
|
|
fun hmacSha256(salt: ByteRange, keyMaterial: ByteRange, plus1: Boolean = false) =
|
|
Mac.getInstance("HMacSHA256").run {
|
|
init(SecretKeySpec(salt.ba, salt.start, salt.size, "HMacSHA256"))
|
|
update(keyMaterial)
|
|
if (plus1) update(hkdfTrailBytes)
|
|
doFinal()
|
|
} ?: error("Mac.doFinal returns null")
|
|
|
|
/**
|
|
* HKDF の2段階目は末尾に \01 を追加する
|
|
*/
|
|
fun hmacSha256Plus1(salt: ByteRange, keyMaterial: ByteRange) =
|
|
hmacSha256(salt, keyMaterial, plus1 = true)
|
|
|
|
// 鍵全体をX509でエンコードしたものをデコードする
|
|
fun Provider.decodePrivateKeyX509(src: ByteArray) =
|
|
KeyFactory.getInstance("EC", this)
|
|
.generatePrivate(PKCS8EncodedKeySpec(src))
|
|
?: error("generatePrivate returns null")
|
|
|
|
/**
|
|
* p256dh(65バイト)から公開鍵を復元する
|
|
* - ECPointUtil.decodePoint はバイト配列全体を指定するしかないので、src引数はByteArrayである
|
|
*/
|
|
fun Provider.decodeP256dh(src: ByteArray, curveName: String = DEFAULT_CURVE_NAME): ECPublicKey {
|
|
val spec = ECNamedCurveTable.getParameterSpec(curveName)
|
|
val params = ECNamedCurveSpec(curveName, spec.curve, spec.g, spec.n)
|
|
val pubKeySpec = ECPublicKeySpec(
|
|
ECPointUtil.decodePoint(params.curve, src),
|
|
params
|
|
)
|
|
return KeyFactory.getInstance("EC", this)
|
|
.generatePublic(pubKeySpec) as ECPublicKey
|
|
}
|
|
|
|
fun Provider.sharedKeyBytes(
|
|
receiverPrivateBytes: ByteArray,
|
|
senderPublicBytes: ByteArray,
|
|
) = KeyAgreement.getInstance("ECDH", this).run {
|
|
val receiverPrivate = decodePrivateKeyX509(receiverPrivateBytes)
|
|
val senderPublic = decodeP256dh(senderPublicBytes)
|
|
init(receiverPrivate)
|
|
doPhase(senderPublic, true)
|
|
generateSecret()
|
|
}.toByteRange()
|