Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.

Expand All @@ -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 <your private key in hex>` 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
Expand Down
5 changes: 4 additions & 1 deletion readme_zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
```

### 加密
Expand Down Expand Up @@ -108,6 +108,9 @@ Nonce将在加密时生成,并必须在解密时通过命令行选项`--nonce`
对于非对称加密,要生成私钥,请用`openssl rand -hex 32`。
要查看对应的公钥,请用`./ctc pubkey <your private key in hex>`。

启用加密时,程序会自动生成一个MAC摘要用以确保信息的完整性。
该摘要会在加密时自动生成,并必须在解密时通过命令行选项`--mac`或`-m`传入。

## 内部设计

### 预处理
Expand Down
31 changes: 31 additions & 0 deletions src/main/kotlin/info/skyblond/ctc/Encryption.kt
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<Int>.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())
}
4 changes: 2 additions & 2 deletions src/main/kotlin/info/skyblond/ctc/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import kotlin.math.absoluteValue
* Return: Code to Char.
* */
fun loadCTC(): Map<Int, String> =
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)
}
Expand Down
11 changes: 11 additions & 0 deletions src/main/kotlin/info/skyblond/ctc/commands/CodecOptions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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)" } }
}
9 changes: 5 additions & 4 deletions src/main/kotlin/info/skyblond/ctc/commands/DecodeCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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()
Expand Down
3 changes: 3 additions & 0 deletions src/main/kotlin/info/skyblond/ctc/commands/EncodeCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>) {
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 ->
Expand Down
Loading