diff --git a/build.gradle b/build.gradle index 10a6ec0a..0c2d02ba 100644 --- a/build.gradle +++ b/build.gradle @@ -10,6 +10,7 @@ buildscript { ext.bouncycastleVersion = '1.69' ext.ed25519Version = '0.3.0' ext.curve25519Version = '0.5.0' + ext.ktlintVersion = '0.45.1' repositories { google() jcenter() @@ -31,3 +32,8 @@ allprojects { task clean(type: Delete) { delete rootProject.buildDir } + +task ktlintFormat(group: "formatting") { + dependsOn ':library:ktlintFormat' + dependsOn ':samples:ktlintFormat' +} diff --git a/library/build.gradle b/library/build.gradle index 52bf8d97..ec5b53ab 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -1,11 +1,10 @@ plugins { id 'java-library' id 'kotlin' + id 'maven' + id 'java' } -apply plugin: 'maven' -apply plugin: 'java' - java { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 @@ -29,7 +28,7 @@ dependencies { implementation "com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:${coroutineAdapterVersion}" implementation "net.i2p.crypto:eddsa:$ed25519Version" implementation "org.whispersystems:curve25519-java:$curve25519Version" - ktlint "com.pinterest:ktlint:0.40.0" + ktlint "com.pinterest:ktlint:$ktlintVersion" testImplementation 'org.jetbrains.kotlin:kotlin-test' testImplementation 'org.jetbrains.kotlin:kotlin-test-junit' diff --git a/library/src/main/kotlin/one/mixin/bot/extension/UUIDExtension.kt b/library/src/main/kotlin/one/mixin/bot/extension/UUIDExtension.kt new file mode 100644 index 00000000..71201baa --- /dev/null +++ b/library/src/main/kotlin/one/mixin/bot/extension/UUIDExtension.kt @@ -0,0 +1,11 @@ +package one.mixin.bot.extension + +import java.nio.ByteBuffer +import java.util.UUID + +fun UUID.toByteArray(): ByteArray { + val bb = ByteBuffer.wrap(ByteArray(16)) + bb.putLong(this.mostSignificantBits) + bb.putLong(this.leastSignificantBits) + return bb.array() +} diff --git a/library/src/main/kotlin/one/mixin/bot/util/ByteArrayUtil.kt b/library/src/main/kotlin/one/mixin/bot/util/ByteArrayUtil.kt new file mode 100644 index 00000000..87fefc73 --- /dev/null +++ b/library/src/main/kotlin/one/mixin/bot/util/ByteArrayUtil.kt @@ -0,0 +1,14 @@ +package one.mixin.bot.util + +@ExperimentalUnsignedTypes +fun toLeByteArray(v: UInt): ByteArray { + val b = ByteArray(2) + b[0] = v.toByte() + b[1] = (v shr 8).toByte() + return b +} + +@ExperimentalUnsignedTypes +fun leByteArrayToInt(bytes: ByteArray): UInt { + return bytes[0].toUInt() + (bytes[1].toUInt() shl 8) +} diff --git a/library/src/main/kotlin/one/mixin/bot/util/CryptoUtil.kt b/library/src/main/kotlin/one/mixin/bot/util/CryptoUtil.kt index e1453576..54475d03 100644 --- a/library/src/main/kotlin/one/mixin/bot/util/CryptoUtil.kt +++ b/library/src/main/kotlin/one/mixin/bot/util/CryptoUtil.kt @@ -1,8 +1,13 @@ +@file:Suppress("unused") + package one.mixin.bot.util import net.i2p.crypto.eddsa.EdDSAPrivateKey +import net.i2p.crypto.eddsa.EdDSAPublicKey +import net.i2p.crypto.eddsa.math.FieldElement import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec +import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec import one.mixin.bot.extension.base64Decode import one.mixin.bot.extension.base64Encode import org.bouncycastle.jce.provider.BouncyCastleProvider @@ -36,9 +41,9 @@ fun generateEd25519KeyPair(): KeyPair { return net.i2p.crypto.eddsa.KeyPairGenerator().generateKeyPair() } -@Throws(IllegalArgumentException::class) -fun calculateAgreement(publicKey: ByteArray, privateKey: EdDSAPrivateKey): ByteArray = - Curve25519.getInstance(Curve25519.BEST).calculateAgreement(publicKey, privateKeyToCurve25519(privateKey.seed)) +fun calculateAgreement(publicKey: ByteArray, privateKey: ByteArray): ByteArray { + return Curve25519.getInstance(Curve25519.BEST).calculateAgreement(publicKey, privateKey) +} fun privateKeyToCurve25519(edSeed: ByteArray): ByteArray { val md = MessageDigest.getInstance("SHA-512") @@ -60,13 +65,13 @@ fun decryASEKey(src: String, privateKey: EdDSAPrivateKey): String? { return Base64.getEncoder().encodeToString( calculateAgreement( Base64.getUrlDecoder().decode(src), - privateKey + privateKeyToCurve25519(privateKey.seed) ) ) } private val secureRandom: SecureRandom = SecureRandom() -private val GCM_IV_LENGTH = 12 +private const val GCM_IV_LENGTH = 12 fun generateAesKey(): ByteArray { val key = ByteArray(16) @@ -74,6 +79,25 @@ fun generateAesKey(): ByteArray { return key } +fun publicKeyToCurve25519(publicKey: EdDSAPublicKey): ByteArray { + val p = publicKey.abyte.map { it.toInt().toByte() }.toByteArray() + val public = EdDSAPublicKey(EdDSAPublicKeySpec(p, ed25519)) + val groupElement = public.a + val x = edwardsToMontgomeryX(groupElement.y) + return x.toByteArray() +} +private fun edwardsToMontgomeryX(y: FieldElement): FieldElement { + val field = ed25519.curve.field + var oneMinusY = field.ONE + oneMinusY = oneMinusY.subtract(y) + oneMinusY = oneMinusY.invert() + + var outX = field.ONE + outX = outX.add(y) + + return oneMinusY.multiply(outX) +} + fun aesGcmEncrypt(plain: ByteArray, key: ByteArray): ByteArray { val iv = ByteArray(GCM_IV_LENGTH) secureRandom.nextBytes(iv) @@ -136,7 +160,7 @@ private fun stripRsaPrivateKeyHeaders(privatePem: String): String { val lines = privatePem.split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() lines.filter { line -> !line.contains("BEGIN RSA PRIVATE KEY") && - !line.contains("END RSA PRIVATE KEY") && !line.trim { it <= ' ' }.isEmpty() + !line.contains("END RSA PRIVATE KEY") && line.trim { it <= ' ' }.isNotEmpty() } .forEach { line -> strippedKey.append(line.trim { it <= ' ' }) } return strippedKey.toString().trim { it <= ' ' } diff --git a/library/src/main/kotlin/one/mixin/bot/util/EncryptedProtocol.kt b/library/src/main/kotlin/one/mixin/bot/util/EncryptedProtocol.kt new file mode 100644 index 00000000..ff6fddff --- /dev/null +++ b/library/src/main/kotlin/one/mixin/bot/util/EncryptedProtocol.kt @@ -0,0 +1,73 @@ +package one.mixin.bot.util + +import net.i2p.crypto.eddsa.EdDSAPrivateKey +import net.i2p.crypto.eddsa.EdDSAPublicKey +import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec +import one.mixin.bot.extension.toByteArray +import java.util.UUID + +class EncryptedProtocol { + + @ExperimentalUnsignedTypes + fun encryptMessage( + privateKey: EdDSAPrivateKey, + plaintext: ByteArray, + otherPublicKey: ByteArray, + otherSessionId: String, + extensionSessionKey: ByteArray? = null, + extensionSessionId: String? = null + ): ByteArray { + val aesGcmKey = generateAesKey() + val encryptedMessageData = aesGcmEncrypt(plaintext, aesGcmKey) + val messageKey = encryptCipherMessageKey(privateKey.seed, otherPublicKey, aesGcmKey) + val messageKeyWithSession = UUID.fromString(otherSessionId).toByteArray().plus(messageKey) + val pub = EdDSAPublicKey(EdDSAPublicKeySpec(privateKey.a, ed25519)) + val senderPublicKey = publicKeyToCurve25519(pub) + val version = byteArrayOf(0x01) + + return if (extensionSessionKey != null && extensionSessionId != null) { + version.plus(toLeByteArray(2.toUInt())).plus(senderPublicKey).let { + val emergencyMessageKey = + encryptCipherMessageKey(privateKey.seed, extensionSessionKey, aesGcmKey) + it.plus(UUID.fromString(extensionSessionId).toByteArray().plus(emergencyMessageKey)) + }.plus(messageKeyWithSession).plus(encryptedMessageData) + } else { + version.plus(toLeByteArray(1.toUInt())).plus(senderPublicKey) + .plus(messageKeyWithSession) + .plus(encryptedMessageData) + } + } + + @ExperimentalUnsignedTypes + fun decryptMessage(privateKey: EdDSAPrivateKey, sessionId: ByteArray, ciphertext: ByteArray): ByteArray { + val sessionSize = leByteArrayToInt(ciphertext.slice(IntRange(1, 2)).toByteArray()).toInt() + val senderPublicKey = ciphertext.slice(IntRange(3, 34)).toByteArray() + var key: ByteArray? = null + repeat(sessionSize) { + val offset = it * 64 + val sid = ciphertext.slice(IntRange(35 + offset, 50 + offset)).toByteArray() + if (sessionId.contentEquals(sid)) { + key = ciphertext.slice(IntRange(51 + offset, 98 + offset)).toByteArray() + } + } + val messageKey = requireNotNull(key) + val message = ciphertext.slice(IntRange(35 + 64 * sessionSize, ciphertext.size - 1)).toByteArray() + val iv = messageKey.slice(IntRange(0, 15)).toByteArray() + val content = messageKey.slice(IntRange(16, messageKey.size - 1)).toByteArray() + val decodedMessageKey = decryptCipherMessageKey(privateKey.seed, senderPublicKey, iv, content) + + return aesGcmDecrypt(message, decodedMessageKey) + } + + private fun encryptCipherMessageKey(seed: ByteArray, publicKey: ByteArray, aesGcmKey: ByteArray): ByteArray { + val private = privateKeyToCurve25519(seed) + val sharedSecret = calculateAgreement(publicKey, private) + return aesEncrypt(sharedSecret, aesGcmKey) + } + + private fun decryptCipherMessageKey(seed: ByteArray, publicKey: ByteArray, iv: ByteArray, ciphertext: ByteArray): ByteArray { + val private = privateKeyToCurve25519(seed) + val sharedSecret = calculateAgreement(publicKey, private) + return aesDecrypt(sharedSecret, iv, ciphertext) + } +} diff --git a/library/src/test/kotlin/one/mixin/bot/EncryptedProtocolTest.kt.kt b/library/src/test/kotlin/one/mixin/bot/EncryptedProtocolTest.kt.kt new file mode 100644 index 00000000..5de96cc0 --- /dev/null +++ b/library/src/test/kotlin/one/mixin/bot/EncryptedProtocolTest.kt.kt @@ -0,0 +1,210 @@ +package one.mixin.bot + +import com.google.gson.Gson +import com.google.gson.annotations.SerializedName +import net.i2p.crypto.eddsa.EdDSAPrivateKey +import net.i2p.crypto.eddsa.EdDSAPublicKey +import one.mixin.bot.extension.base64Encode +import one.mixin.bot.extension.toByteArray +import one.mixin.bot.util.EncryptedProtocol +import one.mixin.bot.util.aesDecrypt +import one.mixin.bot.util.aesEncrypt +import one.mixin.bot.util.base64Decode +import one.mixin.bot.util.calculateAgreement +import one.mixin.bot.util.generateAesKey +import one.mixin.bot.util.generateEd25519KeyPair +import one.mixin.bot.util.privateKeyToCurve25519 +import one.mixin.bot.util.publicKeyToCurve25519 +import java.util.UUID +import kotlin.test.Test +import kotlin.test.assertEquals + +@ExperimentalUnsignedTypes +class EncryptedProtocolTest { + + @Test + fun testText() { + val content = "L".toByteArray() + testEncryptAndDecrypt(content) + } + + @Test + fun testSticker() { + val mockStickerId = UUID.randomUUID().toString() + val mockStickerPayload = StickerMessagePayload(mockStickerId) + val content = Gson().toJson(mockStickerPayload).base64Encode().toByteArray() + testEncryptAndDecrypt(content) + } + + @Test + fun testAes() { + val content = "LA".toByteArray() + val aesGcmKey = generateAesKey() + val encodedContent = aesEncrypt(aesGcmKey, content) + val decryptedContent = aesDecrypt( + aesGcmKey, + encodedContent.slice(IntRange(0, 15)).toByteArray(), + encodedContent.slice(IntRange(16, encodedContent.size - 1)).toByteArray(), + ) + assertEquals("LA", String(decryptedContent)) + } + + @Test + fun testImage() { + val mockAttachmentMessagePayload = AttachmentMessagePayload( + key = base64Decode("2IFv82k/nPZJlQFYRCD7SgWNtDK+Bi5vo0VXhk4A9DAp/RE5r+Shfgn+xEuQiyn8Hjf+Ox9356geoceH926BJQ=="), + digest = base64Decode("z9YuqavioY+hYLB1slFaRzSc9ggBlp+nUOZGHwS8LaU="), + attachmentId = "5a3574ca-cc17-470d-88dc-845613d471b4", + mimeType = "image/jpeg", + height = 949, + width = 1080, + size = 168540, + name = null, + thumbnail = """/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAIQAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3 + NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + lkZXNjAAAA8AAAAHRyWFlaAAABZAAAABRnWFlaAAABeAAAABRiWFlaAAABjAAAABRyVFJDAAABoAAAAChnVFJDAAABoAAAAChiVFJDAAABoAAAACh3dHB0AAABy + AAAABRjcHJ0AAAB3AAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAFgAAAAcAHMAUgBHAEIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA + +EAAC2z3BhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABYWVogAAAAAAAA9tYAAQAAAADTLW1sdWMAAAAAAAAAAQAAAAxlblVTAAAAIAAAABwA + RwBvAG8AZwBsAGUAIABJAG4AYwAuACAAMgAwADEANv/bAEMAAwICAwICAwMDAwQDAwQFCAUFBAQFCgcHBggMCgwMCwoLCw0OEhANDhEOCwsQFhARExQVFRUMDxcYFh + QYEhQVFP/bAEMBAwQEBQQFCQUFCRQNCw0UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFP/AABEIADcAPwMBIgACEQEDEQH/ + xAAbAAACAwEBAQAAAAAAAAAAAAAFBgAECAMHAf/EACsQAAEDAwIEBgIDAAAAAAAAAAEAAgMEBREGEgchMZETFCIyUaEjQRYzQ//EABkBAAMBAQEAAAAAAAAAAAAAAA + IDBAEABf/EAB4RAAICAwEAAwAAAAAAAAAAAAABAhEDEiExBDJR/9oADAMBAAIRAxEAPwABWcHGsB/GOyXLjwl2ZwwdlpG4VMDwdu1LVfGyQnACli2PcfwzhW8L3NziM + dkKm4YSHpF9LRFVbml3QKq22sz7QjcqDWK1ZnaThdLn+n6VeThRLJ/iey0xHa4nOGWDsilHYqZ5G6NvZEm2IlGjO2kOFUtPVsd4JGD8LWvCuwvttExpaR6cLlZrFRR + vB2NXoVn8vSxANwOSkzp0FDh4NS3h1UB6lZMpPPckS3XYQt5uVt+pmtB9f2mvjH3rGxnqJOR5obJVFruTkCl1IDETvQiq1Mxp96H7CsefaWo7w3N7D1XV+qvKjm4DC + 8/GqWBmd4SpqLWfhhwD/tPhJeFeXG6s9pp+JLYZAN47pio+JzS0fk/XysjN1o51R7/38or/ADt8IHrPT5Q5e8IfBnqbm6EHBKXqnUUjHO9RRiekfKDyKWbpbJGhxw + UxxsO7jR0fql/hEb/tBazUUrskOKpupJXPI5q7DYHzR5IWKFEsFrKwfJqmWNhG4pbu+oZKgn1FH7tp18TTyStVWh4kxhCoU7PTl8jaNFWC4yCTOSrc1zkf0X2Gzu + B9qsstbt3RH6Q+mmpraIAcgJZu8LTuGAoojTDfBd8g0y5wEWpYWsYBhRRaKKV2p2SM6JPrbe3xeiiixnHyGhbu6BdPIgSHkFFEs4//2Q== + """.trimMargin() + ) + val content = Gson().toJson(mockAttachmentMessagePayload).toByteArray() + testEncryptAndDecrypt(content) + } + + private fun testEncryptAndDecrypt(content: ByteArray) { + val otherSessionId = UUID.randomUUID().toString() + val encryptedProtocol = EncryptedProtocol() + + val senderKeyPair = generateEd25519KeyPair() + val senderPrivateKey = senderKeyPair.private as EdDSAPrivateKey + + val receiverKeyPair = generateEd25519KeyPair() + val receiverPrivateKey = receiverKeyPair.private as EdDSAPrivateKey + val receiverPublicKey = receiverKeyPair.public as EdDSAPublicKey + val receiverCurvePublicKey = publicKeyToCurve25519(receiverPublicKey) + + val encodedContent = encryptedProtocol.encryptMessage(senderPrivateKey, content, receiverCurvePublicKey, otherSessionId) + + val decryptedContent = encryptedProtocol.decryptMessage(receiverPrivateKey, UUID.fromString(otherSessionId).toByteArray(), encodedContent) + + assert(decryptedContent.contentEquals(content)) + } + + @Test + fun calculateAgreement() { + val senderKeyPair = generateEd25519KeyPair() + val senderPrivateKey = senderKeyPair.private as EdDSAPrivateKey + val senderPublicKey = senderKeyPair.public as EdDSAPublicKey + + val receiverKeyPair = generateEd25519KeyPair() + val receiverPrivateKey = receiverKeyPair.private as EdDSAPrivateKey + val receiverPublicKey = receiverKeyPair.public as EdDSAPublicKey + + val senderPrivate = privateKeyToCurve25519(senderPrivateKey.seed) + val senderSecret = + calculateAgreement(publicKeyToCurve25519(receiverPublicKey), senderPrivate) + + val receiverPrivate = privateKeyToCurve25519(receiverPrivateKey.seed) + val receiverSecret = + calculateAgreement(publicKeyToCurve25519(senderPublicKey), receiverPrivate) + + assert(senderSecret.contentEquals(receiverSecret)) + } +} + +data class StickerMessagePayload( + @SerializedName("sticker_id") + val stickerId: String? = null, + @SerializedName("album_id") + val albumId: String? = null, + @SerializedName("name") + val name: String? = null +) + +data class AttachmentMessagePayload( + @SerializedName("key") + var key: ByteArray?, + @SerializedName("digest") + var digest: ByteArray?, + @SerializedName("attachment_id") + var attachmentId: String, + @SerializedName("mime_type") + var mimeType: String, + @SerializedName("size") + var size: Long, + @SerializedName("name") + var name: String?, + @SerializedName("width") + var width: Int?, + @SerializedName("height") + var height: Int?, + @SerializedName("thumbnail") + var thumbnail: String?, + @SerializedName("duration") + var duration: Long? = null, + @SerializedName("waveform") + var waveform: ByteArray? = null, + @SerializedName("caption") + var caption: String? = null, + @SerializedName("created_at") + var createdAt: String? = null, +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as AttachmentMessagePayload + + if (key != null) { + if (other.key == null) return false + if (!key.contentEquals(other.key)) return false + } else if (other.key != null) return false + if (digest != null) { + if (other.digest == null) return false + if (!digest.contentEquals(other.digest)) return false + } else if (other.digest != null) return false + if (attachmentId != other.attachmentId) return false + if (mimeType != other.mimeType) return false + if (size != other.size) return false + if (name != other.name) return false + if (width != other.width) return false + if (height != other.height) return false + if (thumbnail != other.thumbnail) return false + if (duration != other.duration) return false + if (waveform != null) { + if (other.waveform == null) return false + if (!waveform.contentEquals(other.waveform)) return false + } else if (other.waveform != null) return false + if (caption != other.caption) return false + if (createdAt != other.createdAt) return false + + return true + } + + override fun hashCode(): Int { + var result = key?.contentHashCode() ?: 0 + result = 31 * result + (digest?.contentHashCode() ?: 0) + result = 31 * result + attachmentId.hashCode() + result = 31 * result + mimeType.hashCode() + result = 31 * result + size.hashCode() + result = 31 * result + (name?.hashCode() ?: 0) + result = 31 * result + (width ?: 0) + result = 31 * result + (height ?: 0) + result = 31 * result + (thumbnail?.hashCode() ?: 0) + result = 31 * result + (duration?.hashCode() ?: 0) + result = 31 * result + (waveform?.contentHashCode() ?: 0) + result = 31 * result + (caption?.hashCode() ?: 0) + result = 31 * result + (createdAt?.hashCode() ?: 0) + return result + } +} diff --git a/samples/build.gradle b/samples/build.gradle index a04e04d5..39872ce8 100644 --- a/samples/build.gradle +++ b/samples/build.gradle @@ -2,6 +2,7 @@ plugins { id 'application' id 'kotlin' id 'maven' + id 'java' } java { @@ -9,6 +10,10 @@ java { targetCompatibility = JavaVersion.VERSION_1_8 } +configurations { + ktlint +} + dependencies { implementation project(":library") // implementation 'com.github.MixinNetwork:bot-api-kotlin-client:v0.5.1' @@ -16,4 +21,25 @@ dependencies { // Kotlin implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion" -} \ No newline at end of file + ktlint "com.pinterest:ktlint:$ktlintVersion" +} + +task ktlint(type: JavaExec, group: "verification") { + description = "Check Kotlin code style." + classpath = configurations.ktlint + main = "com.pinterest.ktlint.Main" + args "src/**/*.kt" + // to generate report in checkstyle format prepend following args: + // "--reporter=plain", "--reporter=checkstyle,output=${buildDir}/ktlint.xml" + // to add a baseline to check against prepend following args: + // "--baseline=ktlint-baseline.xml" + // see https://github.com/pinterest/ktlint#usage for more +} +check.dependsOn ktlint + +task ktlintFormat(type: JavaExec, group: "formatting") { + description = "Fix Kotlin code style deviations." + classpath = configurations.ktlint + main = "com.pinterest.ktlint.Main" + args "-F", "src/**/*.kt" +} diff --git a/samples/src/main/java/jvmMain/java/EncryptedProtocolSample.java b/samples/src/main/java/jvmMain/java/EncryptedProtocolSample.java new file mode 100644 index 00000000..43a85dea --- /dev/null +++ b/samples/src/main/java/jvmMain/java/EncryptedProtocolSample.java @@ -0,0 +1,49 @@ +package jvmMain.java; + +import net.i2p.crypto.eddsa.EdDSAPrivateKey; +import net.i2p.crypto.eddsa.EdDSAPublicKey; +import one.mixin.bot.extension.UUIDExtensionKt; +import one.mixin.bot.util.EncryptedProtocol; + +import java.security.KeyPair; +import java.util.Arrays; +import java.util.UUID; + +import static one.mixin.bot.util.CryptoUtilKt.generateEd25519KeyPair; +import static one.mixin.bot.util.CryptoUtilKt.publicKeyToCurve25519; + +class EncryptedProtocolSample { + public static void main(String[] args) { + // init sender key pair + KeyPair senderKeyPair = generateEd25519KeyPair(); + net.i2p.crypto.eddsa.EdDSAPrivateKey senderPrivateKey = (EdDSAPrivateKey) senderKeyPair.getPrivate(); + + // init receiver key pair + KeyPair receiverKeyPair = generateEd25519KeyPair(); + EdDSAPrivateKey receiverPrivateKey = (EdDSAPrivateKey) receiverKeyPair.getPrivate(); + EdDSAPublicKey receiverPublicKey = (EdDSAPublicKey) receiverKeyPair.getPublic(); + byte[] receiverCurvePublicKey = publicKeyToCurve25519(receiverPublicKey); + String receiverSessionId = UUID.randomUUID().toString(); + + // origin message to send + byte[] message = "Hello Mixin".getBytes(); + + EncryptedProtocol encryptedProtocol = new EncryptedProtocol(); + + // encrypt message with receiver's public key and session id + byte[] encryptedMessage = + encryptedProtocol.encryptMessage(senderPrivateKey, message, receiverCurvePublicKey, receiverSessionId, null, null); + + // send to receiver + // ... + + // receive message and decrypt with self private key + byte[] decryptedMessage = encryptedProtocol.decryptMessage( + receiverPrivateKey, + UUIDExtensionKt.toByteArray(UUID.fromString(receiverSessionId)), + encryptedMessage + ); + + System.out.println("decrypted message equals origin message is " + Arrays.equals(decryptedMessage, message)); + } +} \ No newline at end of file diff --git a/samples/src/main/java/jvmMain/java/Sample.java b/samples/src/main/java/jvmMain/java/Sample.java index e53595cd..629097b6 100644 --- a/samples/src/main/java/jvmMain/java/Sample.java +++ b/samples/src/main/java/jvmMain/java/Sample.java @@ -19,6 +19,7 @@ import static one.mixin.bot.util.Base64UtilKt.base64Decode; import static one.mixin.bot.util.CryptoUtilKt.*; +@SuppressWarnings("SameParameterValue") public class Sample { final static String userPin = "131416"; @@ -43,7 +44,7 @@ public static void main(String[] args) { // decrypt pin token String userAesKey; EdDSAPrivateKey userPrivateKey = (EdDSAPrivateKey) sessionKey.getPrivate(); - userAesKey = base64Encode(calculateAgreement(Objects.requireNonNull(base64Decode(user.getPinToken())), userPrivateKey)); + userAesKey = base64Encode(calculateAgreement(Objects.requireNonNull(base64Decode(user.getPinToken())), privateKeyToCurve25519(userPrivateKey.getSeed()))); // get ticker getTicker(client); diff --git a/samples/src/main/java/jvmMain/kotlin/EncryptedProtocolSmaple.kt b/samples/src/main/java/jvmMain/kotlin/EncryptedProtocolSmaple.kt new file mode 100644 index 00000000..e906fc4c --- /dev/null +++ b/samples/src/main/java/jvmMain/kotlin/EncryptedProtocolSmaple.kt @@ -0,0 +1,42 @@ +import net.i2p.crypto.eddsa.EdDSAPrivateKey +import net.i2p.crypto.eddsa.EdDSAPublicKey +import one.mixin.bot.extension.toByteArray +import one.mixin.bot.util.EncryptedProtocol +import one.mixin.bot.util.generateEd25519KeyPair +import one.mixin.bot.util.publicKeyToCurve25519 +import java.util.* + +@ExperimentalUnsignedTypes +fun main() { + // init sender key pair + val senderKeyPair = generateEd25519KeyPair() + val senderPrivateKey = senderKeyPair.private as EdDSAPrivateKey + + // init receiver key pair + val receiverKeyPair = generateEd25519KeyPair() + val receiverPrivateKey = receiverKeyPair.private as EdDSAPrivateKey + val receiverPublicKey = receiverKeyPair.public as EdDSAPublicKey + val receiverCurvePublicKey = publicKeyToCurve25519(receiverPublicKey) + val receiverSessionId = UUID.randomUUID().toString() + + // origin message to send + val message = "Hello Mixin".toByteArray() + + val encryptedProtocol = EncryptedProtocol() + + // encrypt message with receiver's public key and session id + val encryptedMessage = + encryptedProtocol.encryptMessage(senderPrivateKey, message, receiverCurvePublicKey, receiverSessionId) + + // send to receiver + // ... + + // receive message and decrypt with self private key + val decryptedMessage = encryptedProtocol.decryptMessage( + receiverPrivateKey, + UUID.fromString(receiverSessionId).toByteArray(), + encryptedMessage + ) + + println("decrypted message equals origin message is ${decryptedMessage.contentEquals(message)}") +} diff --git a/samples/src/main/java/jvmMain/kotlin/Sample.kt b/samples/src/main/java/jvmMain/kotlin/Sample.kt index 91f07f74..fda4dd14 100644 --- a/samples/src/main/java/jvmMain/kotlin/Sample.kt +++ b/samples/src/main/java/jvmMain/kotlin/Sample.kt @@ -15,7 +15,18 @@ import one.mixin.bot.util.calculateAgreement import one.mixin.bot.util.decryASEKey import one.mixin.bot.util.generateEd25519KeyPair import one.mixin.bot.util.getEdDSAPrivateKeyFromString -import one.mixin.bot.vo.* +import one.mixin.bot.util.privateKeyToCurve25519 +import one.mixin.bot.vo.AccountRequest +import one.mixin.bot.vo.AddressRequest +import one.mixin.bot.vo.GhostKeyRequest +import one.mixin.bot.vo.NetworkSnapshot +import one.mixin.bot.vo.PinRequest +import one.mixin.bot.vo.Snapshot +import one.mixin.bot.vo.TransactionRequest +import one.mixin.bot.vo.TransferRequest +import one.mixin.bot.vo.User +import one.mixin.bot.vo.WithdrawalRequest +import one.mixin.bot.vo.generateTextMessageRequest import java.util.Random import java.util.UUID @@ -47,12 +58,12 @@ fun main() = runBlocking { // decrypt pin token val userAesKey: String val userPrivateKey = sessionKey.private as EdDSAPrivateKey - userAesKey = calculateAgreement(user.pinToken.base64Decode(), userPrivateKey).base64Encode() + userAesKey = calculateAgreement(user.pinToken.base64Decode(), privateKeyToCurve25519(userPrivateKey.seed)).base64Encode() // create user's pin createPin(client, userAesKey) - //Use bot's token + // Use bot's token client.setUserToken(null) // bot transfer to user transferToUser(client, user.userId, pinToken, pin) @@ -86,7 +97,7 @@ fun main() = runBlocking { withdrawalToAddress(client, addressId, userAesKey) } - //Use bot's token + // Use bot's token client.setUserToken(null) // Send text message sendTextMessage(client, "639ec50a-d4f1-4135-8624-3c71189dcdcc", "Text message") @@ -96,7 +107,7 @@ fun main() = runBlocking { networkSnapshots(client, CNB_ID) networkSnapshot(client, "c8e73a02-b543-4100-bd7a-879ed4accdfc") - + readGhostKey(client) return@runBlocking } @@ -218,11 +229,13 @@ private suspend fun withdrawalToAddress( // Withdrawals val withdrawalsResponse = client.snapshotService.withdrawals( WithdrawalRequest( - addressId, DEFAULT_AMOUNT, encryptPin( + addressId, DEFAULT_AMOUNT, + encryptPin( userAesKey, DEFAULT_PIN, System.nanoTime() - ), UUID.randomUUID().toString(), "withdrawal test" + ), + UUID.randomUUID().toString(), "withdrawal test" ) ) if (withdrawalsResponse.isSuccess()) { @@ -312,14 +325,17 @@ internal suspend fun networkSnapshots( } private suspend fun readGhostKey(client: HttpClient) { - val request = GhostKeyRequest(listOf( - "639ec50a-d4f1-4135-8624-3c71189dcdcc", - "d3bee23a-81d4-462e-902a-22dae9ef89ff", - ), 0, "") + val request = GhostKeyRequest( + listOf( + "639ec50a-d4f1-4135-8624-3c71189dcdcc", + "d3bee23a-81d4-462e-902a-22dae9ef89ff", + ), + 0, "" + ) val response = client.userService.readGhostKeys(request) if (response.isSuccess()) { println("ReadGhostKey success ${response.data}") } else { println("ReadGhostKey failure ${response.error}") } -} \ No newline at end of file +} diff --git a/samples/src/main/java/jvmMain/kotlin/UserTransferDemo.kt b/samples/src/main/java/jvmMain/kotlin/UserTransferSample.kt similarity index 94% rename from samples/src/main/java/jvmMain/kotlin/UserTransferDemo.kt rename to samples/src/main/java/jvmMain/kotlin/UserTransferSample.kt index 057b547f..9735a7dc 100644 --- a/samples/src/main/java/jvmMain/kotlin/UserTransferDemo.kt +++ b/samples/src/main/java/jvmMain/kotlin/UserTransferSample.kt @@ -12,6 +12,7 @@ import one.mixin.bot.util.calculateAgreement import one.mixin.bot.util.decryASEKey import one.mixin.bot.util.generateEd25519KeyPair import one.mixin.bot.util.getEdDSAPrivateKeyFromString +import one.mixin.bot.util.privateKeyToCurve25519 fun main() = runBlocking { val key = getEdDSAPrivateKeyFromString(Config.privateKey) @@ -35,7 +36,7 @@ fun main() = runBlocking { client.setUserToken(aliceToken) // decrypt pin token val alicePrivateKey = aliceSessionKey.private as EdDSAPrivateKey - val aliceAesKey = calculateAgreement(alice.pinToken.base64Decode(), alicePrivateKey).base64Encode() + val aliceAesKey = calculateAgreement(alice.pinToken.base64Decode(), privateKeyToCurve25519(alicePrivateKey.seed)).base64Encode() // create alice's pin createPin(client, aliceAesKey) @@ -57,7 +58,7 @@ fun main() = runBlocking { client.setUserToken(bobToken) // decrypt pin token val bobPrivateKey = bobSessionKey.private as EdDSAPrivateKey - val bobAesKey = calculateAgreement(bob.pinToken.base64Decode(), bobPrivateKey).base64Encode() + val bobAesKey = calculateAgreement(bob.pinToken.base64Decode(), privateKeyToCurve25519(bobPrivateKey.seed)).base64Encode() // create bob's pin createPin(client, bobAesKey) @@ -92,7 +93,6 @@ fun main() = runBlocking { assert(bobNetworkSnapshots?.find { it.snapshotId == snapshotAlice2Bob.snapshotId } != null) } - // use bot's token client.setUserToken(null) val botNetworkSnapshot = networkSnapshots(client, CNB_ID, limit = 10) @@ -100,6 +100,6 @@ fun main() = runBlocking { assert(botNetworkSnapshot?.find { it.snapshotId == snapshotBot2Alice.snapshotId } != null) } if (snapshotAlice2Bob != null) { - assert(botNetworkSnapshot?.find { it.snapshotId == snapshotAlice2Bob.snapshotId } != null ) + assert(botNetworkSnapshot?.find { it.snapshotId == snapshotAlice2Bob.snapshotId } != null) } -} \ No newline at end of file +}