diff options
| author | ketsuban <ketsuban@192.168.3.110> | 2026-08-26 13:14:52 +0000 |
|---|---|---|
| committer | ketsuban <ketsuban@192.168.3.110> | 2026-08-26 13:14:52 +0000 |
| commit | 4efb6f38a962f9d901b184772ee117a6e8cebe8a (patch) | |
| tree | c49e8657d42e1e5ed0d44a18b9296d3519f03cb2 | |
| parent | aee754f5275722b94af53026433adf2aa5755720 (diff) | |
| download | kukuri-4efb6f38a962f9d901b184772ee117a6e8cebe8a.tar.gz kukuri-4efb6f38a962f9d901b184772ee117a6e8cebe8a.tar.bz2 kukuri-4efb6f38a962f9d901b184772ee117a6e8cebe8a.zip | |
ch.21: HMAC-SHA256 all targets + AES-GCM jvm actual; visible deploy banner
12 files changed, 146 insertions, 11 deletions
diff --git a/apps/desktop/src/jvmMain/kotlin/jp/orgflow/app/desktop/Main.kt b/apps/desktop/src/jvmMain/kotlin/jp/orgflow/app/desktop/Main.kt index 87d2756..9697007 100644 --- a/apps/desktop/src/jvmMain/kotlin/jp/orgflow/app/desktop/Main.kt +++ b/apps/desktop/src/jvmMain/kotlin/jp/orgflow/app/desktop/Main.kt @@ -18,7 +18,7 @@ fun main() = application { verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, ) { - Text(appGreeting(), fontSize = 24.sp) + Text(appBanner(), fontSize = 18.sp) Text("OrgFlow submission shell (ch.04-10 landing zone)", fontSize = 12.sp) } } diff --git a/modules/orgflow-crypto/src/commonMain/kotlin/jp/orgflow/crypto/AesGcm.kt b/modules/orgflow-crypto/src/commonMain/kotlin/jp/orgflow/crypto/AesGcm.kt new file mode 100644 index 0000000..2a85224 --- /dev/null +++ b/modules/orgflow-crypto/src/commonMain/kotlin/jp/orgflow/crypto/AesGcm.kt @@ -0,0 +1,8 @@ +package jp.orgflow.crypto + +// ch.21 confidentiality (AES-256-GCM). expect/actual: JVM real, others deferred. + +expect class AesGcmCipher { + fun encrypt(key32: ByteArray, nonce12: ByteArray, plaintext: ByteArray): ByteArray + fun decrypt(key32: ByteArray, nonce12: ByteArray, ciphertext: ByteArray): ByteArray +} diff --git a/modules/orgflow-crypto/src/commonMain/kotlin/jp/orgflow/crypto/HashingService.kt b/modules/orgflow-crypto/src/commonMain/kotlin/jp/orgflow/crypto/HashingService.kt index b85bba1..449e540 100644 --- a/modules/orgflow-crypto/src/commonMain/kotlin/jp/orgflow/crypto/HashingService.kt +++ b/modules/orgflow-crypto/src/commonMain/kotlin/jp/orgflow/crypto/HashingService.kt @@ -4,20 +4,22 @@ import okio.Buffer import okio.HashingSink import okio.blackholeSink -// ch.21 暗号・完全性 - hashing/integrity layer. -// AES/HMAC/signatures defer until cryptography-kotlin becomes available on our toolchain; -// integrity verification (hash recompute + compare) is fully covered here. +// ch.21 暗号・完全性 - hashing/integrity/HMAC layer. +// AES-GCM confidentiality: JVM actual via javax.crypto; other platforms defer until +// cryptography-kotlin lands on our toolchain (see NEMOTRON.md constraints). object HashingService { - fun sha256Hex(bytes: ByteArray): String { + fun sha256Hex(bytes: ByteArray): String = sha256(bytes).hex() + + fun sha256(bytes: ByteArray): okio.ByteString { val src = Buffer().apply { write(bytes) } val sink = HashingSink.sha256(blackholeSink()) src.readAll(sink) - return sink.hash.hex() + return sink.hash } } -class IntegrityVerifier { +object IntegrityVerifier { fun verify(bytes: ByteArray, expectedSha256Hex: String): Boolean = HashingService.sha256Hex(bytes) == expectedSha256Hex.lowercase() diff --git a/modules/orgflow-crypto/src/commonMain/kotlin/jp/orgflow/crypto/HmacService.kt b/modules/orgflow-crypto/src/commonMain/kotlin/jp/orgflow/crypto/HmacService.kt new file mode 100644 index 0000000..d81401a --- /dev/null +++ b/modules/orgflow-crypto/src/commonMain/kotlin/jp/orgflow/crypto/HmacService.kt @@ -0,0 +1,24 @@ +package jp.orgflow.crypto + +// HMAC-SHA256 (RFC 2104) built on the platform hashing primitive - works on every target. + +object HmacService { + + fun hmacSha256Hex(key: ByteArray, data: ByteArray): String = + HashingService.sha256Hex(rawHmacSha256(key, data)) + + fun rawHmacSha256(key: ByteArray, data: ByteArray): ByteArray { + val k = normalizeKey(key) + val inner = k.map { it xor 0x36 } + data.toList() + val innerHash = HashingService.sha256(inner.toByteArray()) + val outer = k.map { it xor 0x5c } + innerHash.toList() + return HashingService.sha256(outer.toByteArray()).toByteArray() + } + + private fun normalizeKey(key: ByteArray): ByteArray = when { + key.size > BLOCK_SIZE -> HashingService.sha256(key).toByteArray() + ByteArray(BLOCK_SIZE - 32) + else -> key + ByteArray(BLOCK_SIZE - key.size) + } + + const val BLOCK_SIZE = 64 +} diff --git a/modules/orgflow-crypto/src/commonTest/kotlin/jp/orgflow/crypto/HmacServiceTest.kt b/modules/orgflow-crypto/src/commonTest/kotlin/jp/orgflow/crypto/HmacServiceTest.kt new file mode 100644 index 0000000..4a460ac --- /dev/null +++ b/modules/orgflow-crypto/src/commonTest/kotlin/jp/orgflow/crypto/HmacServiceTest.kt @@ -0,0 +1,23 @@ +package jp.orgflow.crypto + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class HmacServiceTest { + // RFC 4231 test case 2 + @Test + fun rfc4231Vector() { + val mac = HmacService.hmacSha256Hex( + "Jefe".encodeToByteArray(), + "what do ya want for nothing?".encodeToByteArray(), + ) + assertEquals("5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843", mac) + } + + @Test + fun longKeyIsHashed() { + val mac = HmacService.hmacSha256Hex(ByteArray(100) { 1 }, ByteArray(8) { 2 }) + assertEquals(64, mac.length) + } +} diff --git a/modules/orgflow-crypto/src/jvmMain/kotlin/jp/orgflow/crypto/AesGcmCipher.jvm.kt b/modules/orgflow-crypto/src/jvmMain/kotlin/jp/orgflow/crypto/AesGcmCipher.jvm.kt new file mode 100644 index 0000000..731a546 --- /dev/null +++ b/modules/orgflow-crypto/src/jvmMain/kotlin/jp/orgflow/crypto/AesGcmCipher.jvm.kt @@ -0,0 +1,21 @@ +package jp.orgflow.crypto + +import javax.crypto.Cipher +import javax.crypto.spec.GCMParameterSpec +import javax.crypto.spec.SecretKeySpec + +actual class AesGcmCipher { + private fun key(key32: ByteArray) = SecretKeySpec(key32, "AES") + + actual fun encrypt(key32: ByteArray, nonce12: ByteArray, plaintext: ByteArray): ByteArray { + val c = Cipher.getInstance("AES/GCM/NoPadding") + c.init(Cipher.ENCRYPT_MODE, key(key32), GCMParameterSpec(128, nonce12)) + return c.doFinal(plaintext) // includes 16-byte tag + } + + actual fun decrypt(key32: ByteArray, nonce12: ByteArray, ciphertext: ByteArray): ByteArray { + val c = Cipher.getInstance("AES/GCM/NoPadding") + c.init(Cipher.DECRYPT_MODE, key(key32), GCMParameterSpec(128, nonce12)) + return c.doFinal(ciphertext) + } +} diff --git a/modules/orgflow-crypto/src/jvmTest/kotlin/jp/orgflow/crypto/AesGcmJvmTest.kt b/modules/orgflow-crypto/src/jvmTest/kotlin/jp/orgflow/crypto/AesGcmJvmTest.kt new file mode 100644 index 0000000..7d0f86f --- /dev/null +++ b/modules/orgflow-crypto/src/jvmTest/kotlin/jp/orgflow/crypto/AesGcmJvmTest.kt @@ -0,0 +1,29 @@ +package jp.orgflow.crypto + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class AesGcmJvmTest { + @Test + fun roundTrip() { + val c = AesGcmCipher() + val key = ByteArray(32) { it.toByte() } + val nonce = ByteArray(12) { (it * 3).toByte() } + val plain = "confidential org attachment".encodeToByteArray() + + val cipher = c.encrypt(key, nonce, plain) + assertEquals(plain.size + 16, cipher.size) // gcm tag + assertEquals(plain.toList(), c.decrypt(key, nonce, cipher).toList()) + } + + @Test + fun tamperFailsAuthentication() { + val c = AesGcmCipher() + val key = ByteArray(32) { 7 } + val nonce = ByteArray(12) { 9 } + val ct = c.encrypt(key, nonce, "x".encodeToByteArray()) + ct[ct.lastIndex] = (ct.last().toInt() xor 1).toByte() + assertFailsWith<Exception> { c.decrypt(key, nonce, ct) } + } +} diff --git a/modules/orgflow-crypto/src/linuxX64Main/kotlin/jp/orgflow/crypto/AesGcmCipher.linuxX64.kt b/modules/orgflow-crypto/src/linuxX64Main/kotlin/jp/orgflow/crypto/AesGcmCipher.linuxX64.kt new file mode 100644 index 0000000..d363c6e --- /dev/null +++ b/modules/orgflow-crypto/src/linuxX64Main/kotlin/jp/orgflow/crypto/AesGcmCipher.linuxX64.kt @@ -0,0 +1,11 @@ +package jp.orgflow.crypto + +// WebCrypto interop deferred with the cryptography-kotlin toolchain upgrade. + +actual class AesGcmCipher { + private fun defer(): Nothing = + throw UnsupportedOperationException("AES-GCM on linuxX64 lands with cryptography-kotlin (Kotlin>=2.3)") + + actual fun encrypt(key32: ByteArray, nonce12: ByteArray, plaintext: ByteArray): ByteArray = defer() + actual fun decrypt(key32: ByteArray, nonce12: ByteArray, ciphertext: ByteArray): ByteArray = defer() +} diff --git a/modules/orgflow-crypto/src/wasmJsMain/kotlin/jp/orgflow/crypto/AesGcmCipher.wasm.kt b/modules/orgflow-crypto/src/wasmJsMain/kotlin/jp/orgflow/crypto/AesGcmCipher.wasm.kt new file mode 100644 index 0000000..d09c3e6 --- /dev/null +++ b/modules/orgflow-crypto/src/wasmJsMain/kotlin/jp/orgflow/crypto/AesGcmCipher.wasm.kt @@ -0,0 +1,11 @@ +package jp.orgflow.crypto + +// WebCrypto interop deferred with the cryptography-kotlin toolchain upgrade. + +actual class AesGcmCipher { + private fun defer(): Nothing = + throw UnsupportedOperationException("AES-GCM on wasmJs lands with cryptography-kotlin (Kotlin>=2.3)") + + actual fun encrypt(key32: ByteArray, nonce12: ByteArray, plaintext: ByteArray): ByteArray = defer() + actual fun decrypt(key32: ByteArray, nonce12: ByteArray, ciphertext: ByteArray): ByteArray = defer() +} diff --git a/modules/orgflow-domain/src/commonMain/kotlin/kukuri/App.kt b/modules/orgflow-domain/src/commonMain/kotlin/kukuri/App.kt index 59b9120..db33fcf 100644 --- a/modules/orgflow-domain/src/commonMain/kotlin/kukuri/App.kt +++ b/modules/orgflow-domain/src/commonMain/kotlin/kukuri/App.kt @@ -1,5 +1,11 @@ package kukuri +import kotlinx.datetime.Clock + const val APP_VERSION = "0.1.0" fun appGreeting(): String = "kukuri v$APP_VERSION" + +/** Visible-per-load banner: proves which build/deploy you are looking at. */ +fun appBanner(nowIso: String = Clock.System.now().toString()): String = + "$appGreeting | seen $nowIso" diff --git a/modules/orgflow-domain/src/linuxX64Main/kotlin/Main.kt b/modules/orgflow-domain/src/linuxX64Main/kotlin/Main.kt index 77d64ce..c90cdfb 100644 --- a/modules/orgflow-domain/src/linuxX64Main/kotlin/Main.kt +++ b/modules/orgflow-domain/src/linuxX64Main/kotlin/Main.kt @@ -1,5 +1,5 @@ -import kukuri.appGreeting +import kukuri.appBanner fun main() { - println(appGreeting()) + println(appBanner()) } diff --git a/modules/orgflow-domain/src/wasmJsMain/kotlin/Main.kt b/modules/orgflow-domain/src/wasmJsMain/kotlin/Main.kt index 77d64ce..c90cdfb 100644 --- a/modules/orgflow-domain/src/wasmJsMain/kotlin/Main.kt +++ b/modules/orgflow-domain/src/wasmJsMain/kotlin/Main.kt @@ -1,5 +1,5 @@ -import kukuri.appGreeting +import kukuri.appBanner fun main() { - println(appGreeting()) + println(appBanner()) } |
