summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorketsuban <ketsuban@192.168.3.110>2026-08-26 13:14:52 +0000
committerketsuban <ketsuban@192.168.3.110>2026-08-26 13:14:52 +0000
commit4efb6f38a962f9d901b184772ee117a6e8cebe8a (patch)
treec49e8657d42e1e5ed0d44a18b9296d3519f03cb2
parentaee754f5275722b94af53026433adf2aa5755720 (diff)
downloadkukuri-4efb6f38a962f9d901b184772ee117a6e8cebe8a.tar.gz
kukuri-4efb6f38a962f9d901b184772ee117a6e8cebe8a.tar.bz2
kukuri-4efb6f38a962f9d901b184772ee117a6e8cebe8a.zip
ch.21: HMAC-SHA256 all targets + AES-GCM jvm actual; visible deploy banner
-rw-r--r--apps/desktop/src/jvmMain/kotlin/jp/orgflow/app/desktop/Main.kt2
-rw-r--r--modules/orgflow-crypto/src/commonMain/kotlin/jp/orgflow/crypto/AesGcm.kt8
-rw-r--r--modules/orgflow-crypto/src/commonMain/kotlin/jp/orgflow/crypto/HashingService.kt14
-rw-r--r--modules/orgflow-crypto/src/commonMain/kotlin/jp/orgflow/crypto/HmacService.kt24
-rw-r--r--modules/orgflow-crypto/src/commonTest/kotlin/jp/orgflow/crypto/HmacServiceTest.kt23
-rw-r--r--modules/orgflow-crypto/src/jvmMain/kotlin/jp/orgflow/crypto/AesGcmCipher.jvm.kt21
-rw-r--r--modules/orgflow-crypto/src/jvmTest/kotlin/jp/orgflow/crypto/AesGcmJvmTest.kt29
-rw-r--r--modules/orgflow-crypto/src/linuxX64Main/kotlin/jp/orgflow/crypto/AesGcmCipher.linuxX64.kt11
-rw-r--r--modules/orgflow-crypto/src/wasmJsMain/kotlin/jp/orgflow/crypto/AesGcmCipher.wasm.kt11
-rw-r--r--modules/orgflow-domain/src/commonMain/kotlin/kukuri/App.kt6
-rw-r--r--modules/orgflow-domain/src/linuxX64Main/kotlin/Main.kt4
-rw-r--r--modules/orgflow-domain/src/wasmJsMain/kotlin/Main.kt4
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())
}