Skip to content

Commit 5045a1f

Browse files
authored
Add encrypted protocol related code, tests, samples (#4)
* Add encrypted protocol related code, tests, samples * Fix ktlint
1 parent 9b6a46a commit 5045a1f

File tree

13 files changed

+500
-29
lines changed

13 files changed

+500
-29
lines changed

build.gradle

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ buildscript {
1010
ext.bouncycastleVersion = '1.69'
1111
ext.ed25519Version = '0.3.0'
1212
ext.curve25519Version = '0.5.0'
13+
ext.ktlintVersion = '0.45.1'
1314
repositories {
1415
google()
1516
jcenter()
@@ -31,3 +32,8 @@ allprojects {
3132
task clean(type: Delete) {
3233
delete rootProject.buildDir
3334
}
35+
36+
task ktlintFormat(group: "formatting") {
37+
dependsOn ':library:ktlintFormat'
38+
dependsOn ':samples:ktlintFormat'
39+
}

library/build.gradle

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
plugins {
22
id 'java-library'
33
id 'kotlin'
4+
id 'maven'
5+
id 'java'
46
}
57

6-
apply plugin: 'maven'
7-
apply plugin: 'java'
8-
98
java {
109
sourceCompatibility = JavaVersion.VERSION_1_8
1110
targetCompatibility = JavaVersion.VERSION_1_8
@@ -29,7 +28,7 @@ dependencies {
2928
implementation "com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:${coroutineAdapterVersion}"
3029
implementation "net.i2p.crypto:eddsa:$ed25519Version"
3130
implementation "org.whispersystems:curve25519-java:$curve25519Version"
32-
ktlint "com.pinterest:ktlint:0.40.0"
31+
ktlint "com.pinterest:ktlint:$ktlintVersion"
3332

3433
testImplementation 'org.jetbrains.kotlin:kotlin-test'
3534
testImplementation 'org.jetbrains.kotlin:kotlin-test-junit'
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package one.mixin.bot.extension
2+
3+
import java.nio.ByteBuffer
4+
import java.util.UUID
5+
6+
fun UUID.toByteArray(): ByteArray {
7+
val bb = ByteBuffer.wrap(ByteArray(16))
8+
bb.putLong(this.mostSignificantBits)
9+
bb.putLong(this.leastSignificantBits)
10+
return bb.array()
11+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package one.mixin.bot.util
2+
3+
@ExperimentalUnsignedTypes
4+
fun toLeByteArray(v: UInt): ByteArray {
5+
val b = ByteArray(2)
6+
b[0] = v.toByte()
7+
b[1] = (v shr 8).toByte()
8+
return b
9+
}
10+
11+
@ExperimentalUnsignedTypes
12+
fun leByteArrayToInt(bytes: ByteArray): UInt {
13+
return bytes[0].toUInt() + (bytes[1].toUInt() shl 8)
14+
}

library/src/main/kotlin/one/mixin/bot/util/CryptoUtil.kt

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1+
@file:Suppress("unused")
2+
13
package one.mixin.bot.util
24

35
import net.i2p.crypto.eddsa.EdDSAPrivateKey
6+
import net.i2p.crypto.eddsa.EdDSAPublicKey
7+
import net.i2p.crypto.eddsa.math.FieldElement
48
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable
59
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec
10+
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec
611
import one.mixin.bot.extension.base64Decode
712
import one.mixin.bot.extension.base64Encode
813
import org.bouncycastle.jce.provider.BouncyCastleProvider
@@ -36,9 +41,9 @@ fun generateEd25519KeyPair(): KeyPair {
3641
return net.i2p.crypto.eddsa.KeyPairGenerator().generateKeyPair()
3742
}
3843

39-
@Throws(IllegalArgumentException::class)
40-
fun calculateAgreement(publicKey: ByteArray, privateKey: EdDSAPrivateKey): ByteArray =
41-
Curve25519.getInstance(Curve25519.BEST).calculateAgreement(publicKey, privateKeyToCurve25519(privateKey.seed))
44+
fun calculateAgreement(publicKey: ByteArray, privateKey: ByteArray): ByteArray {
45+
return Curve25519.getInstance(Curve25519.BEST).calculateAgreement(publicKey, privateKey)
46+
}
4247

4348
fun privateKeyToCurve25519(edSeed: ByteArray): ByteArray {
4449
val md = MessageDigest.getInstance("SHA-512")
@@ -60,20 +65,39 @@ fun decryASEKey(src: String, privateKey: EdDSAPrivateKey): String? {
6065
return Base64.getEncoder().encodeToString(
6166
calculateAgreement(
6267
Base64.getUrlDecoder().decode(src),
63-
privateKey
68+
privateKeyToCurve25519(privateKey.seed)
6469
)
6570
)
6671
}
6772

6873
private val secureRandom: SecureRandom = SecureRandom()
69-
private val GCM_IV_LENGTH = 12
74+
private const val GCM_IV_LENGTH = 12
7075

7176
fun generateAesKey(): ByteArray {
7277
val key = ByteArray(16)
7378
secureRandom.nextBytes(key)
7479
return key
7580
}
7681

82+
fun publicKeyToCurve25519(publicKey: EdDSAPublicKey): ByteArray {
83+
val p = publicKey.abyte.map { it.toInt().toByte() }.toByteArray()
84+
val public = EdDSAPublicKey(EdDSAPublicKeySpec(p, ed25519))
85+
val groupElement = public.a
86+
val x = edwardsToMontgomeryX(groupElement.y)
87+
return x.toByteArray()
88+
}
89+
private fun edwardsToMontgomeryX(y: FieldElement): FieldElement {
90+
val field = ed25519.curve.field
91+
var oneMinusY = field.ONE
92+
oneMinusY = oneMinusY.subtract(y)
93+
oneMinusY = oneMinusY.invert()
94+
95+
var outX = field.ONE
96+
outX = outX.add(y)
97+
98+
return oneMinusY.multiply(outX)
99+
}
100+
77101
fun aesGcmEncrypt(plain: ByteArray, key: ByteArray): ByteArray {
78102
val iv = ByteArray(GCM_IV_LENGTH)
79103
secureRandom.nextBytes(iv)
@@ -136,7 +160,7 @@ private fun stripRsaPrivateKeyHeaders(privatePem: String): String {
136160
val lines = privatePem.split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
137161
lines.filter { line ->
138162
!line.contains("BEGIN RSA PRIVATE KEY") &&
139-
!line.contains("END RSA PRIVATE KEY") && !line.trim { it <= ' ' }.isEmpty()
163+
!line.contains("END RSA PRIVATE KEY") && line.trim { it <= ' ' }.isNotEmpty()
140164
}
141165
.forEach { line -> strippedKey.append(line.trim { it <= ' ' }) }
142166
return strippedKey.toString().trim { it <= ' ' }
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package one.mixin.bot.util
2+
3+
import net.i2p.crypto.eddsa.EdDSAPrivateKey
4+
import net.i2p.crypto.eddsa.EdDSAPublicKey
5+
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec
6+
import one.mixin.bot.extension.toByteArray
7+
import java.util.UUID
8+
9+
class EncryptedProtocol {
10+
11+
@ExperimentalUnsignedTypes
12+
fun encryptMessage(
13+
privateKey: EdDSAPrivateKey,
14+
plaintext: ByteArray,
15+
otherPublicKey: ByteArray,
16+
otherSessionId: String,
17+
extensionSessionKey: ByteArray? = null,
18+
extensionSessionId: String? = null
19+
): ByteArray {
20+
val aesGcmKey = generateAesKey()
21+
val encryptedMessageData = aesGcmEncrypt(plaintext, aesGcmKey)
22+
val messageKey = encryptCipherMessageKey(privateKey.seed, otherPublicKey, aesGcmKey)
23+
val messageKeyWithSession = UUID.fromString(otherSessionId).toByteArray().plus(messageKey)
24+
val pub = EdDSAPublicKey(EdDSAPublicKeySpec(privateKey.a, ed25519))
25+
val senderPublicKey = publicKeyToCurve25519(pub)
26+
val version = byteArrayOf(0x01)
27+
28+
return if (extensionSessionKey != null && extensionSessionId != null) {
29+
version.plus(toLeByteArray(2.toUInt())).plus(senderPublicKey).let {
30+
val emergencyMessageKey =
31+
encryptCipherMessageKey(privateKey.seed, extensionSessionKey, aesGcmKey)
32+
it.plus(UUID.fromString(extensionSessionId).toByteArray().plus(emergencyMessageKey))
33+
}.plus(messageKeyWithSession).plus(encryptedMessageData)
34+
} else {
35+
version.plus(toLeByteArray(1.toUInt())).plus(senderPublicKey)
36+
.plus(messageKeyWithSession)
37+
.plus(encryptedMessageData)
38+
}
39+
}
40+
41+
@ExperimentalUnsignedTypes
42+
fun decryptMessage(privateKey: EdDSAPrivateKey, sessionId: ByteArray, ciphertext: ByteArray): ByteArray {
43+
val sessionSize = leByteArrayToInt(ciphertext.slice(IntRange(1, 2)).toByteArray()).toInt()
44+
val senderPublicKey = ciphertext.slice(IntRange(3, 34)).toByteArray()
45+
var key: ByteArray? = null
46+
repeat(sessionSize) {
47+
val offset = it * 64
48+
val sid = ciphertext.slice(IntRange(35 + offset, 50 + offset)).toByteArray()
49+
if (sessionId.contentEquals(sid)) {
50+
key = ciphertext.slice(IntRange(51 + offset, 98 + offset)).toByteArray()
51+
}
52+
}
53+
val messageKey = requireNotNull(key)
54+
val message = ciphertext.slice(IntRange(35 + 64 * sessionSize, ciphertext.size - 1)).toByteArray()
55+
val iv = messageKey.slice(IntRange(0, 15)).toByteArray()
56+
val content = messageKey.slice(IntRange(16, messageKey.size - 1)).toByteArray()
57+
val decodedMessageKey = decryptCipherMessageKey(privateKey.seed, senderPublicKey, iv, content)
58+
59+
return aesGcmDecrypt(message, decodedMessageKey)
60+
}
61+
62+
private fun encryptCipherMessageKey(seed: ByteArray, publicKey: ByteArray, aesGcmKey: ByteArray): ByteArray {
63+
val private = privateKeyToCurve25519(seed)
64+
val sharedSecret = calculateAgreement(publicKey, private)
65+
return aesEncrypt(sharedSecret, aesGcmKey)
66+
}
67+
68+
private fun decryptCipherMessageKey(seed: ByteArray, publicKey: ByteArray, iv: ByteArray, ciphertext: ByteArray): ByteArray {
69+
val private = privateKeyToCurve25519(seed)
70+
val sharedSecret = calculateAgreement(publicKey, private)
71+
return aesDecrypt(sharedSecret, iv, ciphertext)
72+
}
73+
}

0 commit comments

Comments
 (0)