From b95180712cd92fe592900866d6dbb8edd8b0b050 Mon Sep 17 00:00:00 2001 From: Rui Hu Date: Sat, 10 Jan 2026 20:09:04 +0800 Subject: [PATCH] Add SipHash --- README.md | 8 +++-- readme_zh-CN.md | 5 ++- .../kotlin/info/skyblond/ctc/Encryption.kt | 31 +++++++++++++++++++ src/main/kotlin/info/skyblond/ctc/Utils.kt | 4 +-- .../skyblond/ctc/commands/CodecOptions.kt | 11 +++++++ .../skyblond/ctc/commands/DecodeCommand.kt | 9 +++--- .../skyblond/ctc/commands/EncodeCommand.kt | 3 ++ .../skyblond/ctc/{Fetch.kt => FetchCTC.kt} | 4 +-- 8 files changed, 64 insertions(+), 11 deletions(-) rename src/test/kotlin/info/skyblond/ctc/{Fetch.kt => FetchCTC.kt} (93%) diff --git a/README.md b/README.md index b64a9b2..4c3bdb7 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ So don't worry if you accidentally typed a letter or something. Keep typing numbers and you're good. ```bash -./ctc decode -t "1131, 0561, 2676, 7230, 4767, 9976, 5363, 5212, 0701, 5934, 1226, 9975" +./ctc decode -t "1131 0561 2676 7230 4767 9976 5363 5212 0701 5934 1226 9975 84" ``` ### Encryption @@ -109,7 +109,8 @@ When decrypting, the same stream of fake codes will be generated using the same code = (enc - key + 10000) % 10000 ``` -The nonce will be generated at the encrypting time, and is required via cli option `--nonce` (`-n` for short). +The nonce will be generated at the encrypting time, +and is required via cli option `--nonce` (`-n` for short) during the decrypting time. The key (`-k` or `--key`) will be decoded differently depending on the encryption mode. @@ -124,6 +125,9 @@ The result will be the ChaCha20 stream key. For asymmetric encryption, to generate the private key, use `openssl rand -hex 32`. To get the public key, use `./ctc pubkey ` to calculate the public key. +When encryption is enabled, the program will also generate an MAC to ensure the integrity of the message. +This will be generated on encrypting time, and will be required on decrypting time via `--mac hex` (`-m` for short). + ## Internal design ### Preprocess diff --git a/readme_zh-CN.md b/readme_zh-CN.md index 8f4eec9..9fab14f 100644 --- a/readme_zh-CN.md +++ b/readme_zh-CN.md @@ -74,7 +74,7 @@ echo "天匠染青红,花腰呈袅娜。" | ./ctc encode -c UTF-8 不用担心,继续敲就好了。 ```bash -./ctc decode -t "1131, 0561, 2676, 7230, 4767, 9976, 5363, 5212, 0701, 5934, 1226, 9975" +./ctc decode -t "1131 0561 2676 7230 4767 9976 5363 5212 0701 5934 1226 9975 84" ``` ### 加密 @@ -108,6 +108,9 @@ Nonce将在加密时生成,并必须在解密时通过命令行选项`--nonce` 对于非对称加密,要生成私钥,请用`openssl rand -hex 32`。 要查看对应的公钥,请用`./ctc pubkey `。 +启用加密时,程序会自动生成一个MAC摘要用以确保信息的完整性。 +该摘要会在加密时自动生成,并必须在解密时通过命令行选项`--mac`或`-m`传入。 + ## 内部设计 ### 预处理 diff --git a/src/main/kotlin/info/skyblond/ctc/Encryption.kt b/src/main/kotlin/info/skyblond/ctc/Encryption.kt index 1546504..736d646 100644 --- a/src/main/kotlin/info/skyblond/ctc/Encryption.kt +++ b/src/main/kotlin/info/skyblond/ctc/Encryption.kt @@ -1,6 +1,10 @@ package info.skyblond.ctc +import org.bouncycastle.crypto.digests.SHA256Digest import org.bouncycastle.crypto.engines.ChaCha7539Engine +import org.bouncycastle.crypto.generators.HKDFBytesGenerator +import org.bouncycastle.crypto.macs.SipHash +import org.bouncycastle.crypto.params.HKDFParameters import org.bouncycastle.crypto.params.KeyParameter import org.bouncycastle.crypto.params.ParametersWithIV import org.bouncycastle.util.Pack @@ -65,4 +69,31 @@ fun generateNonce(): ByteArray { val p2 = Pack.intToBigEndian(SecureRandom().nextInt()) return p1 + p2 +} + +/** + * Generate a 128bit key from the given 32-byte key for SipHash. + * */ +fun ByteArray.hkdfSipHashKey(): ByteArray { + require(this.size == 32) { "Key must be 32 bytes (256 bits)" } + val hkdf = HKDFBytesGenerator(SHA256Digest()) + val params = HKDFParameters(this, null, "SipHash".encodeToByteArray()) + hkdf.init(params) + val output = ByteArray(16) + hkdf.generateBytes(output, 0, 16) + return output +} + +/** + * Calculate SipHash MAC for the give list of CTC code. + * */ +fun List.sipHash(key: ByteArray): ByteArray { + require(key.size == 16) { "Key must be 16 bytes (128 bits)" } + val mac = SipHash() + mac.init(KeyParameter(key)) + + for (i in this) { + mac.update(Pack.shortToBigEndian( i.toShort()), 0, 2) + } + return Pack.longToBigEndian(mac.doFinal()) } \ No newline at end of file diff --git a/src/main/kotlin/info/skyblond/ctc/Utils.kt b/src/main/kotlin/info/skyblond/ctc/Utils.kt index 95eb9f4..50813dd 100644 --- a/src/main/kotlin/info/skyblond/ctc/Utils.kt +++ b/src/main/kotlin/info/skyblond/ctc/Utils.kt @@ -20,8 +20,8 @@ import kotlin.math.absoluteValue * Return: Code to Char. * */ fun loadCTC(): Map = - object {}.javaClass.getResourceAsStream("/ctc.txt")!!.use { - it.reader().readLines() + object {}.javaClass.getResourceAsStream("/ctc.txt")!!.use { ins -> + ins.reader().readLines().filterNot { it.isBlank() || it.startsWith("#") } }.associate { it.take(4).toInt() to it.drop(4) } diff --git a/src/main/kotlin/info/skyblond/ctc/commands/CodecOptions.kt b/src/main/kotlin/info/skyblond/ctc/commands/CodecOptions.kt index a3434db..2c7a191 100644 --- a/src/main/kotlin/info/skyblond/ctc/commands/CodecOptions.kt +++ b/src/main/kotlin/info/skyblond/ctc/commands/CodecOptions.kt @@ -64,6 +64,12 @@ class CodecOptions : OptionGroup( "This will be generated when encrypting, and is required when decrypting." } + private val mac by option("-m", "--mac", metavar = "hex") + .help { + "The mac for encrypted codes, in hex format. Must be 8 bytes (64 bits). " + + "This will be generated when encrypting, and is required when decrypting." + } + /** * If dh is not enabled, treat key as utf8 string and apply sha3 256 as the encryption key. * @@ -88,4 +94,9 @@ class CodecOptions : OptionGroup( ?.replace(" ", "") ?.decodeHex() ?.apply { require(this.size == 12) { "nonce must be 12 bytes (96 bits)" } } + + fun getMac(): ByteArray? = mac + ?.replace(" ", "") + ?.decodeHex() + ?.apply { require(this.size == 8) { "mac must be 8 bytes (64 bits)" } } } diff --git a/src/main/kotlin/info/skyblond/ctc/commands/DecodeCommand.kt b/src/main/kotlin/info/skyblond/ctc/commands/DecodeCommand.kt index 0050da5..63a60e3 100644 --- a/src/main/kotlin/info/skyblond/ctc/commands/DecodeCommand.kt +++ b/src/main/kotlin/info/skyblond/ctc/commands/DecodeCommand.kt @@ -6,10 +6,7 @@ import com.github.ajalt.clikt.parameters.groups.provideDelegate import com.github.ajalt.clikt.parameters.options.flag import com.github.ajalt.clikt.parameters.options.help import com.github.ajalt.clikt.parameters.options.option -import info.skyblond.ctc.createChacha20CodeSeq -import info.skyblond.ctc.decodeCTC -import info.skyblond.ctc.decrypt -import info.skyblond.ctc.verifyISO7064 +import info.skyblond.ctc.* object DecodeCommand : CliktCommand( name = "decode" @@ -46,6 +43,10 @@ object DecodeCommand : CliktCommand( val secret = codecOptions.getSecret() if (secret != null) { val nonce = codecOptions.getNonce() ?: error("Nonce is required for decryption") + val sipHashKey = secret.hkdfSipHashKey() + val mac = codecOptions.getMac() ?: error("MAC is required for decryption") + val calculatedSipHash = codes.sipHash(sipHashKey) + require(calculatedSipHash.contentEquals(mac)) { "MAC mismatch" } codes = codes.asSequence().decrypt( createChacha20CodeSeq(secret, nonce) ).toList() diff --git a/src/main/kotlin/info/skyblond/ctc/commands/EncodeCommand.kt b/src/main/kotlin/info/skyblond/ctc/commands/EncodeCommand.kt index b4c888e..927cb56 100644 --- a/src/main/kotlin/info/skyblond/ctc/commands/EncodeCommand.kt +++ b/src/main/kotlin/info/skyblond/ctc/commands/EncodeCommand.kt @@ -32,10 +32,13 @@ object EncodeCommand : CliktCommand( val secret = codecOptions.getSecret() if (secret != null) { val nonce = generateNonce() + val sipHashKey = secret.hkdfSipHashKey() echo("Nonce = ${nonce.toHexString().uppercase().chunked(4).joinToString(" ")}") codes = codes.asSequence().encrypt( createChacha20CodeSeq(secret, nonce) ).toList() + val mac = codes.sipHash(sipHashKey) + echo("MAC = ${mac.toHexString().uppercase().chunked(4).joinToString(" ")}") } codes.toCTCString().let { s -> diff --git a/src/test/kotlin/info/skyblond/ctc/Fetch.kt b/src/test/kotlin/info/skyblond/ctc/FetchCTC.kt similarity index 93% rename from src/test/kotlin/info/skyblond/ctc/Fetch.kt rename to src/test/kotlin/info/skyblond/ctc/FetchCTC.kt index b08fb93..72e0a00 100644 --- a/src/test/kotlin/info/skyblond/ctc/Fetch.kt +++ b/src/test/kotlin/info/skyblond/ctc/FetchCTC.kt @@ -3,12 +3,12 @@ package info.skyblond.ctc import org.jsoup.Jsoup import java.io.File -object Fetch { +object FetchCTC { @JvmStatic fun main(args: Array) { val body = Jsoup .connect("https://en.wiktionary.org/wiki/Appendix:Chinese_telegraph_code/Mainland_1983") - .proxy("127.0.0.1", 1081) + .proxy("127.0.0.1", 2080) .get().body() val map = body.getElementsByTag("tbody").flatMap { tbody -> tbody.getElementsByTag("td").mapNotNull { td ->