feat: replace libsodium with pure Kotlin ChaCha20/Poly1305 implementation
Remove LazySodium/JNA/libsodium native dependencies and replace with pure Kotlin implementations of ChaCha20, HChaCha20, XChaCha20, Poly1305 MAC, and XChaCha20-Poly1305 AEAD. This eliminates platform- specific native binaries while maintaining full NIP-44 compatibility. - Add ChaCha20Core with RFC 8439 block function, IETF stream XOR, HChaCha20, and XChaCha20 - Add Poly1305 MAC (RFC 8439 §2.5) - Add XChaCha20Poly1305 AEAD encrypt/decrypt - Convert LibSodiumInstance from expect/actual to commonMain object - Delete platform-specific LibSodiumInstance actuals (android/jvm/ios) - Remove iOS native interop (cinterop, linker opts, .a libraries) - Remove lazysodium-java, lazysodium-android, JNA from build deps - Clean up ProGuard rules (remove JNA/LazySodium keep rules) - Add RFC 8439 + libsodium test vectors for ChaCha20 and XChaCha20 - Update benchmark to use simplified ChaCha20 API https://claude.ai/code/session_01YHBwqnb3Z7Q7PX31tJ2G3i
This commit is contained in:
+4
-24
@@ -52,36 +52,16 @@ class ChaCha20Benchmark {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun encryptLibSodium() {
|
fun encrypt() {
|
||||||
benchmarkRule.measureRepeated {
|
benchmarkRule.measureRepeated {
|
||||||
chaCha.encryptLibSodium(padded, messageKeys.chachaNonce, messageKeys.chachaKey)
|
chaCha.encrypt(padded, messageKeys.chachaNonce, messageKeys.chachaKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
Removed in the conversion to KMP
|
|
||||||
@Test
|
@Test
|
||||||
fun encryptNative() {
|
fun decrypt() {
|
||||||
benchmarkRule.measureRepeated {
|
benchmarkRule.measureRepeated {
|
||||||
chaCha.encryptNative(padded, messageKeys.chachaNonce, messageKeys.chachaKey)
|
chaCha.decrypt(padded, messageKeys.chachaNonce, messageKeys.chachaKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun decryptLibSodium() {
|
|
||||||
benchmarkRule.measureRepeated {
|
|
||||||
chaCha.decryptLibSodium(padded, messageKeys.chachaNonce, messageKeys.chachaKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Removed in the conversion to KMP
|
|
||||||
@Test
|
|
||||||
fun decryptNative() {
|
|
||||||
benchmarkRule.measureRepeated {
|
|
||||||
chaCha.decryptNative(padded, messageKeys.chachaNonce, messageKeys.chachaKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ fragmentKtx = "1.8.9"
|
|||||||
gms = "4.4.4"
|
gms = "4.4.4"
|
||||||
jacksonModuleKotlin = "2.21.1"
|
jacksonModuleKotlin = "2.21.1"
|
||||||
javaKeyring = "1.0.4"
|
javaKeyring = "1.0.4"
|
||||||
jna = "5.18.1"
|
|
||||||
jtorctl = "0.4.5.7"
|
jtorctl = "0.4.5.7"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
kchesslib = "1.0.5"
|
kchesslib = "1.0.5"
|
||||||
@@ -34,8 +33,6 @@ kotlinxCollectionsImmutable = "0.4.0"
|
|||||||
kotlinxCoroutinesCore = "1.10.2"
|
kotlinxCoroutinesCore = "1.10.2"
|
||||||
kotlinxSerialization = "1.10.0"
|
kotlinxSerialization = "1.10.0"
|
||||||
languageId = "17.0.6"
|
languageId = "17.0.6"
|
||||||
lazysodiumAndroid = "5.2.0"
|
|
||||||
lazysodiumJava = "5.2.0"
|
|
||||||
lifecycleRuntimeKtx = "2.10.0"
|
lifecycleRuntimeKtx = "2.10.0"
|
||||||
lightcompressor-enhanced = "1.6.0"
|
lightcompressor-enhanced = "1.6.0"
|
||||||
markdown = "f92ef49c9d"
|
markdown = "f92ef49c9d"
|
||||||
@@ -144,7 +141,6 @@ google-mlkit-language-id = { group = "com.google.mlkit", name = "language-id", v
|
|||||||
google-mlkit-translate = { group = "com.google.mlkit", name = "translate", version.ref = "translate" }
|
google-mlkit-translate = { group = "com.google.mlkit", name = "translate", version.ref = "translate" }
|
||||||
jackson-module-kotlin = { group = "com.fasterxml.jackson.module", name = "jackson-module-kotlin", version.ref = "jacksonModuleKotlin" }
|
jackson-module-kotlin = { group = "com.fasterxml.jackson.module", name = "jackson-module-kotlin", version.ref = "jacksonModuleKotlin" }
|
||||||
java-keyring = { group = "com.github.javakeyring", name = "java-keyring", version.ref = "javaKeyring" }
|
java-keyring = { group = "com.github.javakeyring", name = "java-keyring", version.ref = "javaKeyring" }
|
||||||
jna = { group = "net.java.dev.jna", name = "jna", version.ref = "jna" }
|
|
||||||
jtorctl = { module = "info.guardianproject:jtorctl", version.ref = "jtorctl" }
|
jtorctl = { module = "info.guardianproject:jtorctl", version.ref = "jtorctl" }
|
||||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||||
kchesslib = { module = "io.github.cvb941:kchesslib", version.ref = "kchesslib" }
|
kchesslib = { module = "io.github.cvb941:kchesslib", version.ref = "kchesslib" }
|
||||||
@@ -153,8 +149,6 @@ kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-c
|
|||||||
kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinxCoroutinesCore" }
|
kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinxCoroutinesCore" }
|
||||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
|
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
|
||||||
kotlinx-serialization-cbor = { module = "org.jetbrains.kotlinx:kotlinx-serialization-cbor", version.ref = "kotlinxSerialization" }
|
kotlinx-serialization-cbor = { module = "org.jetbrains.kotlinx:kotlinx-serialization-cbor", version.ref = "kotlinxSerialization" }
|
||||||
lazysodium-java = { group = "com.goterl", name = "lazysodium-java", version.ref = "lazysodiumJava" }
|
|
||||||
lazysodium-android = { group = "com.goterl", name = "lazysodium-android", version.ref = "lazysodiumAndroid" }
|
|
||||||
markdown-commonmark = { group = "com.github.vitorpamplona.compose-richtext", name = "richtext-commonmark", version.ref = "markdown" }
|
markdown-commonmark = { group = "com.github.vitorpamplona.compose-richtext", name = "richtext-commonmark", version.ref = "markdown" }
|
||||||
markdown-ui = { group = "com.github.vitorpamplona.compose-richtext", name = "richtext-ui", version.ref = "markdown" }
|
markdown-ui = { group = "com.github.vitorpamplona.compose-richtext", name = "richtext-ui", version.ref = "markdown" }
|
||||||
markdown-ui-material3 = { group = "com.github.vitorpamplona.compose-richtext", name = "richtext-ui-material3", version.ref = "markdown" }
|
markdown-ui-material3 = { group = "com.github.vitorpamplona.compose-richtext", name = "richtext-ui-material3", version.ref = "markdown" }
|
||||||
|
|||||||
@@ -63,65 +63,11 @@ kotlin {
|
|||||||
// project can be found here:
|
// project can be found here:
|
||||||
// https://developer.android.com/kotlin/multiplatform/migrate
|
// https://developer.android.com/kotlin/multiplatform/migrate
|
||||||
val xcfName = "quartz-kmpKit"
|
val xcfName = "quartz-kmpKit"
|
||||||
val libsodiumPath = project.file("src/nativeInterop/libsodium")
|
|
||||||
val libsodiumHeaderFilesPath = project.file("$libsodiumPath/include/sodium")
|
|
||||||
|
|
||||||
// Generate target-specific Libsodium definition files for creating native bindings.
|
|
||||||
// Device (iosArm64) uses libsodium.a, simulator targets use libsodium-simulator.a.
|
|
||||||
val libsodiumDeviceDefFile =
|
|
||||||
project.layout.buildDirectory
|
|
||||||
.file("cinterop/Clibsodium-device.def")
|
|
||||||
.get()
|
|
||||||
.asFile
|
|
||||||
val libsodiumSimulatorDefFile =
|
|
||||||
project.layout.buildDirectory
|
|
||||||
.file("cinterop/Clibsodium-simulator.def")
|
|
||||||
.get()
|
|
||||||
.asFile
|
|
||||||
|
|
||||||
// This generates the Libsodium definition file, necessary for creating native bindings(a Kotlin API) for libsodium(for iOS).
|
|
||||||
val libsodiumDefFileGeneration =
|
|
||||||
tasks.register("GenerateSodiumCinteropFile") {
|
|
||||||
outputs.files(libsodiumDeviceDefFile, libsodiumSimulatorDefFile)
|
|
||||||
doLast {
|
|
||||||
libsodiumDeviceDefFile.parentFile.mkdirs()
|
|
||||||
libsodiumDeviceDefFile.writeText(
|
|
||||||
"package = Clibsodium\n" +
|
|
||||||
"staticLibraries = libsodium.a\n" +
|
|
||||||
"libraryPaths = ${libsodiumPath.absolutePath}/ios/lib\n",
|
|
||||||
)
|
|
||||||
libsodiumSimulatorDefFile.writeText(
|
|
||||||
"package = Clibsodium\n" +
|
|
||||||
"staticLibraries = libsodium-simulator.a\n" +
|
|
||||||
"libraryPaths = ${libsodiumPath.absolutePath}/ios-simulators/lib\n",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
listOf(
|
listOf(
|
||||||
iosArm64(),
|
iosArm64(),
|
||||||
iosSimulatorArm64(),
|
iosSimulatorArm64(),
|
||||||
).forEach { target ->
|
).forEach { target ->
|
||||||
val isSimulator = target.name != "iosArm64"
|
|
||||||
val defFile = if (isSimulator) libsodiumSimulatorDefFile else libsodiumDeviceDefFile
|
|
||||||
|
|
||||||
target.compilations.getByName("main") {
|
|
||||||
val clibsodium by cinterops.creating {
|
|
||||||
definitionFile = defFile
|
|
||||||
packageName = "Clibsodium"
|
|
||||||
|
|
||||||
headers(
|
|
||||||
"$libsodiumHeaderFilesPath/crypto_aead_xchacha20poly1305.h",
|
|
||||||
"$libsodiumHeaderFilesPath/crypto_core_hchacha20.h",
|
|
||||||
"$libsodiumHeaderFilesPath/crypto_stream_chacha20.h",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.named(cinterops.getByName("clibsodium").interopProcessingTaskName).configure {
|
|
||||||
dependsOn(libsodiumDefFileGeneration)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
target.swiftPackageConfig(cinteropName = "swiftbridge") {
|
target.swiftPackageConfig(cinteropName = "swiftbridge") {
|
||||||
minIos = "17"
|
minIos = "17"
|
||||||
minMacos = "14"
|
minMacos = "14"
|
||||||
@@ -139,9 +85,6 @@ kotlin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
iosArm64 {
|
iosArm64 {
|
||||||
binaries.all {
|
|
||||||
linkerOpts("-L${libsodiumPath.absolutePath}/ios/lib", "-lsodium")
|
|
||||||
}
|
|
||||||
binaries.framework {
|
binaries.framework {
|
||||||
baseName = xcfName
|
baseName = xcfName
|
||||||
binaryOption("bundleId", "com.vitorpamplona.quartz")
|
binaryOption("bundleId", "com.vitorpamplona.quartz")
|
||||||
@@ -149,9 +92,6 @@ kotlin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
iosSimulatorArm64 {
|
iosSimulatorArm64 {
|
||||||
binaries.all {
|
|
||||||
linkerOpts("-L${libsodiumPath.absolutePath}/ios-simulators/lib", "-lsodium-simulator")
|
|
||||||
}
|
|
||||||
binaries.framework {
|
binaries.framework {
|
||||||
baseName = xcfName
|
baseName = xcfName
|
||||||
binaryOption("bundleId", "com.vitorpamplona.quartz")
|
binaryOption("bundleId", "com.vitorpamplona.quartz")
|
||||||
@@ -254,9 +194,6 @@ kotlin {
|
|||||||
// Bitcoin secp256k1 bindings
|
// Bitcoin secp256k1 bindings
|
||||||
implementation(libs.secp256k1.kmp.jni.jvm)
|
implementation(libs.secp256k1.kmp.jni.jvm)
|
||||||
|
|
||||||
// LibSodium for ChaCha encryption (NIP-44)
|
|
||||||
implementation(libs.lazysodium.java)
|
|
||||||
implementation(libs.jna)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,9 +216,6 @@ kotlin {
|
|||||||
// Bitcoin secp256k1 bindings to Android
|
// Bitcoin secp256k1 bindings to Android
|
||||||
api(libs.secp256k1.kmp.jni.android)
|
api(libs.secp256k1.kmp.jni.android)
|
||||||
|
|
||||||
// LibSodium for ChaCha encryption (NIP-44)
|
|
||||||
implementation("com.goterl:lazysodium-android:5.2.0@aar")
|
|
||||||
implementation("net.java.dev.jna:jna:5.18.1@aar")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,9 +227,6 @@ kotlin {
|
|||||||
// Bitcoin secp256k1 bindings
|
// Bitcoin secp256k1 bindings
|
||||||
implementation(libs.secp256k1.kmp.jni.jvm)
|
implementation(libs.secp256k1.kmp.jni.jvm)
|
||||||
|
|
||||||
// LibSodium for ChaCha encryption (NIP-44) - Needed for host tests
|
|
||||||
implementation(libs.lazysodium.java)
|
|
||||||
implementation(libs.jna)
|
|
||||||
|
|
||||||
// SQLite bundled driver for Host tests
|
// SQLite bundled driver for Host tests
|
||||||
implementation(libs.androidx.sqlite.bundled.jvm)
|
implementation(libs.androidx.sqlite.bundled.jvm)
|
||||||
@@ -315,9 +246,6 @@ kotlin {
|
|||||||
// Bitcoin secp256k1 bindings to Android
|
// Bitcoin secp256k1 bindings to Android
|
||||||
api(libs.secp256k1.kmp.jni.android)
|
api(libs.secp256k1.kmp.jni.android)
|
||||||
|
|
||||||
// LibSodium for ChaCha encryption (NIP-44)
|
|
||||||
implementation("com.goterl:lazysodium-android:5.2.0@aar")
|
|
||||||
implementation("net.java.dev.jna:jna:5.18.1@aar")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,32 +16,10 @@
|
|||||||
# preserve access to native classses
|
# preserve access to native classses
|
||||||
-keep class fr.acinq.secp256k1.** { *; }
|
-keep class fr.acinq.secp256k1.** { *; }
|
||||||
|
|
||||||
# JNA For Libsodium
|
|
||||||
-keep class com.goterl.lazysodium.** { *; }
|
|
||||||
|
|
||||||
# libscrypt
|
# libscrypt
|
||||||
-keep class com.lambdaworks.codec.** { *; }
|
-keep class com.lambdaworks.codec.** { *; }
|
||||||
-keep class com.lambdaworks.crypto.** { *; }
|
-keep class com.lambdaworks.crypto.** { *; }
|
||||||
-keep class com.lambdaworks.jni.** { *; }
|
-keep class com.lambdaworks.jni.** { *; }
|
||||||
|
|
||||||
# JNA also requires AWT, which Android does not have. So the classes are broken down to filter AWT out
|
|
||||||
-keep class com.sun.jna.ToNativeConverter { *; }
|
|
||||||
-keep class com.sun.jna.NativeMapped { *; }
|
|
||||||
-keep class com.sun.jna.CallbackReference { *; }
|
|
||||||
-keep class com.sun.jna.ptr.IntByReference { *; }
|
|
||||||
-keep class com.sun.jna.NativeLong { *; }
|
|
||||||
-keep class com.sun.jna.Structure { *; }
|
|
||||||
-keep class com.sun.jna.Structure$* { *; }
|
|
||||||
-keep class com.sun.jna.Native$ffi_callback { *; }
|
|
||||||
-keep class * implements com.sun.jna.Structure$* { *; }
|
|
||||||
-keep class * implements com.sun.jna.Native$* { *; }
|
|
||||||
-keep class com.sun.jna.Native {
|
|
||||||
private static com.sun.jna.NativeMapped fromNative(java.lang.Class, java.lang.Object);
|
|
||||||
private static com.sun.jna.NativeMapped fromNative(java.lang.reflect.Method, java.lang.Object);
|
|
||||||
private static java.lang.Class nativeType(java.lang.Class);
|
|
||||||
private static java.lang.Object toNative(com.sun.jna.ToNativeConverter, java.lang.Object);
|
|
||||||
private static java.lang.Object fromNative(com.sun.jna.FromNativeConverter, java.lang.Object, java.lang.reflect.Method);
|
|
||||||
}
|
|
||||||
|
|
||||||
# JSON parsing
|
# JSON parsing
|
||||||
-keep class com.vitorpamplona.quartz.** { *; }
|
-keep class com.vitorpamplona.quartz.** { *; }
|
||||||
Vendored
-22
@@ -30,33 +30,11 @@
|
|||||||
# preserve access to native classses
|
# preserve access to native classses
|
||||||
-keep class fr.acinq.secp256k1.** { *; }
|
-keep class fr.acinq.secp256k1.** { *; }
|
||||||
|
|
||||||
# JNA For Libsodium
|
|
||||||
-keep class com.goterl.lazysodium.** { *; }
|
|
||||||
|
|
||||||
# libscrypt
|
# libscrypt
|
||||||
-keep class com.lambdaworks.codec.** { *; }
|
-keep class com.lambdaworks.codec.** { *; }
|
||||||
-keep class com.lambdaworks.crypto.** { *; }
|
-keep class com.lambdaworks.crypto.** { *; }
|
||||||
-keep class com.lambdaworks.jni.** { *; }
|
-keep class com.lambdaworks.jni.** { *; }
|
||||||
|
|
||||||
# JNA also requires AWT, which Android does not have. So the classes are broken down to filter AWT out
|
|
||||||
-keep class com.sun.jna.ToNativeConverter { *; }
|
|
||||||
-keep class com.sun.jna.NativeMapped { *; }
|
|
||||||
-keep class com.sun.jna.CallbackReference { *; }
|
|
||||||
-keep class com.sun.jna.ptr.IntByReference { *; }
|
|
||||||
-keep class com.sun.jna.NativeLong { *; }
|
|
||||||
-keep class com.sun.jna.Structure { *; }
|
|
||||||
-keep class com.sun.jna.Structure$* { *; }
|
|
||||||
-keep class com.sun.jna.Native$ffi_callback { *; }
|
|
||||||
-keep class * implements com.sun.jna.Structure$* { *; }
|
|
||||||
-keep class * implements com.sun.jna.Native$* { *; }
|
|
||||||
-keep class com.sun.jna.Native {
|
|
||||||
private static com.sun.jna.NativeMapped fromNative(java.lang.Class, java.lang.Object);
|
|
||||||
private static com.sun.jna.NativeMapped fromNative(java.lang.reflect.Method, java.lang.Object);
|
|
||||||
private static java.lang.Class nativeType(java.lang.Class);
|
|
||||||
private static java.lang.Object toNative(com.sun.jna.ToNativeConverter, java.lang.Object);
|
|
||||||
private static java.lang.Object fromNative(com.sun.jna.FromNativeConverter, java.lang.Object, java.lang.reflect.Method);
|
|
||||||
}
|
|
||||||
|
|
||||||
# JSON parsing
|
# JSON parsing
|
||||||
-keep class com.vitorpamplona.quartz.** { *; }
|
-keep class com.vitorpamplona.quartz.** { *; }
|
||||||
|
|
||||||
|
|||||||
-131
@@ -1,131 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025 Vitor Pamplona
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
* this software and associated documentation files (the "Software"), to deal in
|
|
||||||
* the Software without restriction, including without limitation the rights to use,
|
|
||||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
|
||||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
|
||||||
* subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
||||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
||||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
|
||||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
||||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
package com.vitorpamplona.quartz.utils
|
|
||||||
|
|
||||||
import com.goterl.lazysodium.LazySodium
|
|
||||||
import com.goterl.lazysodium.LazySodiumAndroid
|
|
||||||
import com.goterl.lazysodium.Sodium
|
|
||||||
import com.goterl.lazysodium.SodiumAndroid
|
|
||||||
|
|
||||||
actual object LibSodiumInstance {
|
|
||||||
private val libSodium: Sodium =
|
|
||||||
try {
|
|
||||||
// If we are running in a host test, SodiumJava might be available.
|
|
||||||
// SodiumJava uses a ResourceLoader to find the dylib/so/dll in the jar.
|
|
||||||
Class
|
|
||||||
.forName("com.goterl.lazysodium.SodiumJava")
|
|
||||||
.getConstructor()
|
|
||||||
.newInstance() as Sodium
|
|
||||||
} catch (_: Exception) {
|
|
||||||
SodiumAndroid()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val lazySodium: LazySodium =
|
|
||||||
if (libSodium is SodiumAndroid) {
|
|
||||||
LazySodiumAndroid(libSodium)
|
|
||||||
} else {
|
|
||||||
// this should only happen on test cases
|
|
||||||
val sodiumJava =
|
|
||||||
Class
|
|
||||||
.forName("com.goterl.lazysodium.SodiumJava")
|
|
||||||
|
|
||||||
Class
|
|
||||||
.forName("com.goterl.lazysodium.LazySodiumJava")
|
|
||||||
.getConstructor(sodiumJava)
|
|
||||||
.newInstance(libSodium) as LazySodium
|
|
||||||
}
|
|
||||||
|
|
||||||
actual fun cryptoAeadXChaCha20Poly1305IetfDecrypt(
|
|
||||||
message: ByteArray,
|
|
||||||
nSec: ByteArray,
|
|
||||||
ciphertext: ByteArray,
|
|
||||||
ad: ByteArray,
|
|
||||||
nPub: ByteArray,
|
|
||||||
k: ByteArray,
|
|
||||||
): Boolean =
|
|
||||||
lazySodium.cryptoAeadXChaCha20Poly1305IetfDecrypt(
|
|
||||||
message,
|
|
||||||
longArrayOf(message.size.toLong()),
|
|
||||||
nSec,
|
|
||||||
ciphertext,
|
|
||||||
ciphertext.size.toLong(),
|
|
||||||
ad,
|
|
||||||
ad.size.toLong(),
|
|
||||||
nPub,
|
|
||||||
k,
|
|
||||||
)
|
|
||||||
|
|
||||||
actual fun cryptoAeadXChaCha20Poly1305IetfEncrypt(
|
|
||||||
ciphertext: ByteArray,
|
|
||||||
message: ByteArray,
|
|
||||||
ad: ByteArray,
|
|
||||||
nSec: ByteArray,
|
|
||||||
nPub: ByteArray,
|
|
||||||
k: ByteArray,
|
|
||||||
): Boolean =
|
|
||||||
lazySodium.cryptoAeadXChaCha20Poly1305IetfEncrypt(
|
|
||||||
ciphertext,
|
|
||||||
longArrayOf(ciphertext.size.toLong()),
|
|
||||||
message,
|
|
||||||
message.size.toLong(),
|
|
||||||
ad,
|
|
||||||
ad.size.toLong(),
|
|
||||||
nSec,
|
|
||||||
nPub,
|
|
||||||
k,
|
|
||||||
)
|
|
||||||
|
|
||||||
actual fun cryptoStreamChaCha20IetfXor(
|
|
||||||
message: ByteArray,
|
|
||||||
nonce: ByteArray?,
|
|
||||||
key: ByteArray?,
|
|
||||||
): ByteArray {
|
|
||||||
val ciphertext = ByteArray(message.size)
|
|
||||||
lazySodium.cryptoStreamChaCha20IetfXor(ciphertext, message, message.size.toLong(), nonce, key)
|
|
||||||
return ciphertext
|
|
||||||
}
|
|
||||||
|
|
||||||
// This function wasn't available in the bindings library. I had to move them here from C
|
|
||||||
actual fun cryptoStreamXChaCha20Xor(
|
|
||||||
messageBytes: ByteArray,
|
|
||||||
nonce: ByteArray,
|
|
||||||
key: ByteArray,
|
|
||||||
): ByteArray {
|
|
||||||
val cipher = ByteArray(messageBytes.size)
|
|
||||||
val k2 = ByteArray(32)
|
|
||||||
|
|
||||||
val nonceChaCha = nonce.drop(16).toByteArray()
|
|
||||||
assert(nonceChaCha.size == 8)
|
|
||||||
|
|
||||||
libSodium.crypto_core_hchacha20(k2, nonce, key, null)
|
|
||||||
val resultCode =
|
|
||||||
libSodium.crypto_stream_chacha20_xor_ic(
|
|
||||||
cipher,
|
|
||||||
messageBytes,
|
|
||||||
messageBytes.size.toLong(),
|
|
||||||
nonceChaCha,
|
|
||||||
0,
|
|
||||||
k2,
|
|
||||||
)
|
|
||||||
|
|
||||||
return if (resultCode == 0) cipher else throw IllegalStateException("Could not decrypt message")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+2
-41
@@ -20,55 +20,16 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.quartz.nip44Encryption.crypto
|
package com.vitorpamplona.quartz.nip44Encryption.crypto
|
||||||
|
|
||||||
import com.vitorpamplona.quartz.utils.LibSodiumInstance
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encapsulates the ChaCha20 options. LibSodium is faster on real hardware: 851ns vs 2,535ns encrypt/decrypt times
|
|
||||||
*/
|
|
||||||
class ChaCha20 {
|
class ChaCha20 {
|
||||||
fun encrypt(
|
fun encrypt(
|
||||||
message: ByteArray,
|
message: ByteArray,
|
||||||
nonce: ByteArray,
|
nonce: ByteArray,
|
||||||
key: ByteArray,
|
key: ByteArray,
|
||||||
) = encryptLibSodium(message, nonce, key)
|
) = ChaCha20Core.chaCha20Xor(message, key, nonce, counter = 0)
|
||||||
|
|
||||||
fun decrypt(
|
fun decrypt(
|
||||||
message: ByteArray,
|
message: ByteArray,
|
||||||
nonce: ByteArray,
|
nonce: ByteArray,
|
||||||
key: ByteArray,
|
key: ByteArray,
|
||||||
) = decryptLibSodium(message, nonce, key)
|
) = ChaCha20Core.chaCha20Xor(message, key, nonce, counter = 0)
|
||||||
|
|
||||||
/*
|
|
||||||
fun encryptNative(
|
|
||||||
message: ByteArray,
|
|
||||||
nonce: ByteArray,
|
|
||||||
key: ByteArray,
|
|
||||||
): ByteArray {
|
|
||||||
val cipher = Cipher.getInstance("ChaCha20")
|
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, FixedKey(key, "ChaCha20"), IvParameterSpec(nonce))
|
|
||||||
return cipher.doFinal(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun decryptNative(
|
|
||||||
message: ByteArray,
|
|
||||||
nonce: ByteArray,
|
|
||||||
key: ByteArray,
|
|
||||||
): ByteArray {
|
|
||||||
val cipher = Cipher.getInstance("ChaCha20")
|
|
||||||
cipher.init(Cipher.DECRYPT_MODE, FixedKey(key, "ChaCha20"), IvParameterSpec(nonce))
|
|
||||||
return cipher.doFinal(message)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
fun encryptLibSodium(
|
|
||||||
message: ByteArray,
|
|
||||||
nonce: ByteArray,
|
|
||||||
key: ByteArray,
|
|
||||||
) = LibSodiumInstance.cryptoStreamChaCha20IetfXor(message, nonce, key)
|
|
||||||
|
|
||||||
fun decryptLibSodium(
|
|
||||||
message: ByteArray,
|
|
||||||
nonce: ByteArray,
|
|
||||||
key: ByteArray,
|
|
||||||
) = LibSodiumInstance.cryptoStreamChaCha20IetfXor(message, nonce, key)
|
|
||||||
}
|
}
|
||||||
|
|||||||
+266
@@ -0,0 +1,266 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.quartz.nip44Encryption.crypto
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pure Kotlin implementation of ChaCha20, HChaCha20, and XChaCha20.
|
||||||
|
*
|
||||||
|
* All functions are stateless and thread-safe — they use only local variables
|
||||||
|
* and produce no side effects beyond their return values.
|
||||||
|
*
|
||||||
|
* References:
|
||||||
|
* - RFC 8439: ChaCha20 and Poly1305
|
||||||
|
* - draft-irtf-cfrg-xchacha: XChaCha20 (HChaCha20 + ChaCha20)
|
||||||
|
*/
|
||||||
|
object ChaCha20Core {
|
||||||
|
// "expand 32-byte k" as little-endian integers
|
||||||
|
private const val SIGMA0 = 0x61707865.toInt()
|
||||||
|
private const val SIGMA1 = 0x3320646e.toInt()
|
||||||
|
private const val SIGMA2 = 0x79622d32.toInt()
|
||||||
|
private const val SIGMA3 = 0x6b206574.toInt()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ChaCha20 quarter round (RFC 8439 §2.1).
|
||||||
|
* Operates in-place on the 16-word state.
|
||||||
|
*/
|
||||||
|
private fun quarterRound(
|
||||||
|
state: IntArray,
|
||||||
|
a: Int,
|
||||||
|
b: Int,
|
||||||
|
c: Int,
|
||||||
|
d: Int,
|
||||||
|
) {
|
||||||
|
state[a] += state[b]; state[d] = (state[d] xor state[a]).rotateLeft(16)
|
||||||
|
state[c] += state[d]; state[b] = (state[b] xor state[c]).rotateLeft(12)
|
||||||
|
state[a] += state[b]; state[d] = (state[d] xor state[a]).rotateLeft(8)
|
||||||
|
state[c] += state[d]; state[b] = (state[b] xor state[c]).rotateLeft(7)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform 20 rounds (10 double-rounds) of ChaCha on the state.
|
||||||
|
*/
|
||||||
|
private fun chaCha20Rounds(state: IntArray) {
|
||||||
|
repeat(10) {
|
||||||
|
// Column rounds
|
||||||
|
quarterRound(state, 0, 4, 8, 12)
|
||||||
|
quarterRound(state, 1, 5, 9, 13)
|
||||||
|
quarterRound(state, 2, 6, 10, 14)
|
||||||
|
quarterRound(state, 3, 7, 11, 15)
|
||||||
|
// Diagonal rounds
|
||||||
|
quarterRound(state, 0, 5, 10, 15)
|
||||||
|
quarterRound(state, 1, 6, 11, 12)
|
||||||
|
quarterRound(state, 2, 7, 8, 13)
|
||||||
|
quarterRound(state, 3, 4, 9, 14)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the ChaCha20 state matrix from key, counter, and nonce.
|
||||||
|
* Layout (RFC 8439 §2.3):
|
||||||
|
* cccccccc cccccccc cccccccc cccccccc
|
||||||
|
* kkkkkkkk kkkkkkkk kkkkkkkk kkkkkkkk
|
||||||
|
* kkkkkkkk kkkkkkkk kkkkkkkk kkkkkkkk
|
||||||
|
* bbbbbbbb nnnnnnnn nnnnnnnn nnnnnnnn
|
||||||
|
*/
|
||||||
|
private fun initState(
|
||||||
|
key: ByteArray,
|
||||||
|
counter: Int,
|
||||||
|
nonce: ByteArray,
|
||||||
|
): IntArray {
|
||||||
|
val state = IntArray(16)
|
||||||
|
state[0] = SIGMA0
|
||||||
|
state[1] = SIGMA1
|
||||||
|
state[2] = SIGMA2
|
||||||
|
state[3] = SIGMA3
|
||||||
|
state[4] = key.littleEndianToInt(0)
|
||||||
|
state[5] = key.littleEndianToInt(4)
|
||||||
|
state[6] = key.littleEndianToInt(8)
|
||||||
|
state[7] = key.littleEndianToInt(12)
|
||||||
|
state[8] = key.littleEndianToInt(16)
|
||||||
|
state[9] = key.littleEndianToInt(20)
|
||||||
|
state[10] = key.littleEndianToInt(24)
|
||||||
|
state[11] = key.littleEndianToInt(28)
|
||||||
|
state[12] = counter
|
||||||
|
state[13] = nonce.littleEndianToInt(0)
|
||||||
|
state[14] = nonce.littleEndianToInt(4)
|
||||||
|
state[15] = nonce.littleEndianToInt(8)
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ChaCha20 block function (RFC 8439 §2.3).
|
||||||
|
* Produces a 64-byte keystream block.
|
||||||
|
*
|
||||||
|
* @param key 32-byte key
|
||||||
|
* @param counter block counter
|
||||||
|
* @param nonce 12-byte nonce (IETF variant)
|
||||||
|
* @return 64-byte keystream block
|
||||||
|
*/
|
||||||
|
fun chaCha20Block(
|
||||||
|
key: ByteArray,
|
||||||
|
counter: Int,
|
||||||
|
nonce: ByteArray,
|
||||||
|
): ByteArray {
|
||||||
|
val initial = initState(key, counter, nonce)
|
||||||
|
val working = initial.copyOf()
|
||||||
|
chaCha20Rounds(working)
|
||||||
|
|
||||||
|
// Add initial state to working state
|
||||||
|
for (i in 0..15) {
|
||||||
|
working[i] += initial[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize to little-endian bytes
|
||||||
|
val output = ByteArray(64)
|
||||||
|
for (i in 0..15) {
|
||||||
|
working[i].intToLittleEndian(output, i * 4)
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ChaCha20 IETF stream cipher XOR (RFC 8439 §2.4).
|
||||||
|
* XORs the message with the ChaCha20 keystream.
|
||||||
|
*
|
||||||
|
* @param message plaintext or ciphertext
|
||||||
|
* @param key 32-byte key
|
||||||
|
* @param nonce 12-byte nonce
|
||||||
|
* @param counter initial block counter (0 for stream cipher, 1 for AEAD)
|
||||||
|
* @return XOR'd output (same length as message)
|
||||||
|
*/
|
||||||
|
fun chaCha20Xor(
|
||||||
|
message: ByteArray,
|
||||||
|
key: ByteArray,
|
||||||
|
nonce: ByteArray,
|
||||||
|
counter: Int = 0,
|
||||||
|
): ByteArray {
|
||||||
|
val output = ByteArray(message.size)
|
||||||
|
val fullBlocks = message.size / 64
|
||||||
|
val remainder = message.size % 64
|
||||||
|
|
||||||
|
for (i in 0 until fullBlocks) {
|
||||||
|
val block = chaCha20Block(key, counter + i, nonce)
|
||||||
|
val offset = i * 64
|
||||||
|
for (j in 0..63) {
|
||||||
|
output[offset + j] = (message[offset + j].toInt() xor block[j].toInt()).toByte()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainder > 0) {
|
||||||
|
val block = chaCha20Block(key, counter + fullBlocks, nonce)
|
||||||
|
val offset = fullBlocks * 64
|
||||||
|
for (j in 0 until remainder) {
|
||||||
|
output[offset + j] = (message[offset + j].toInt() xor block[j].toInt()).toByte()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HChaCha20 (draft-irtf-cfrg-xchacha §2.2).
|
||||||
|
* Derives a 32-byte subkey from a 32-byte key and 16-byte input.
|
||||||
|
* Used as the first step of XChaCha20.
|
||||||
|
*
|
||||||
|
* @param key 32-byte key
|
||||||
|
* @param input 16-byte input (first 16 bytes of the 24-byte XChaCha20 nonce)
|
||||||
|
* @return 32-byte subkey
|
||||||
|
*/
|
||||||
|
fun hChaCha20(
|
||||||
|
key: ByteArray,
|
||||||
|
input: ByteArray,
|
||||||
|
): ByteArray {
|
||||||
|
val state = IntArray(16)
|
||||||
|
state[0] = SIGMA0
|
||||||
|
state[1] = SIGMA1
|
||||||
|
state[2] = SIGMA2
|
||||||
|
state[3] = SIGMA3
|
||||||
|
state[4] = key.littleEndianToInt(0)
|
||||||
|
state[5] = key.littleEndianToInt(4)
|
||||||
|
state[6] = key.littleEndianToInt(8)
|
||||||
|
state[7] = key.littleEndianToInt(12)
|
||||||
|
state[8] = key.littleEndianToInt(16)
|
||||||
|
state[9] = key.littleEndianToInt(20)
|
||||||
|
state[10] = key.littleEndianToInt(24)
|
||||||
|
state[11] = key.littleEndianToInt(28)
|
||||||
|
state[12] = input.littleEndianToInt(0)
|
||||||
|
state[13] = input.littleEndianToInt(4)
|
||||||
|
state[14] = input.littleEndianToInt(8)
|
||||||
|
state[15] = input.littleEndianToInt(12)
|
||||||
|
|
||||||
|
chaCha20Rounds(state)
|
||||||
|
|
||||||
|
// Output words 0-3 and 12-15 (skip the key material in 4-11)
|
||||||
|
val output = ByteArray(32)
|
||||||
|
state[0].intToLittleEndian(output, 0)
|
||||||
|
state[1].intToLittleEndian(output, 4)
|
||||||
|
state[2].intToLittleEndian(output, 8)
|
||||||
|
state[3].intToLittleEndian(output, 12)
|
||||||
|
state[12].intToLittleEndian(output, 16)
|
||||||
|
state[13].intToLittleEndian(output, 20)
|
||||||
|
state[14].intToLittleEndian(output, 24)
|
||||||
|
state[15].intToLittleEndian(output, 28)
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* XChaCha20 stream cipher XOR (draft-irtf-cfrg-xchacha §2.3).
|
||||||
|
* Uses HChaCha20 to derive a subkey, then applies ChaCha20 with the remaining nonce bytes.
|
||||||
|
*
|
||||||
|
* @param message plaintext or ciphertext
|
||||||
|
* @param nonce 24-byte nonce
|
||||||
|
* @param key 32-byte key
|
||||||
|
* @return XOR'd output (same length as message)
|
||||||
|
*/
|
||||||
|
fun xChaCha20Xor(
|
||||||
|
message: ByteArray,
|
||||||
|
nonce: ByteArray,
|
||||||
|
key: ByteArray,
|
||||||
|
): ByteArray {
|
||||||
|
// Step 1: Derive subkey using first 16 bytes of nonce
|
||||||
|
val subKey = hChaCha20(key, nonce.copyOfRange(0, 16))
|
||||||
|
|
||||||
|
// Step 2: Build 12-byte subnonce: 4 zero bytes + last 8 bytes of nonce
|
||||||
|
val subNonce = ByteArray(12)
|
||||||
|
nonce.copyInto(subNonce, destinationOffset = 4, startIndex = 16, endIndex = 24)
|
||||||
|
|
||||||
|
// Step 3: Encrypt with ChaCha20 using subkey, counter=0
|
||||||
|
return chaCha20Xor(message, subKey, subNonce, counter = 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Little-endian conversion helpers ---
|
||||||
|
|
||||||
|
private fun ByteArray.littleEndianToInt(offset: Int): Int =
|
||||||
|
(this[offset].toInt() and 0xFF) or
|
||||||
|
((this[offset + 1].toInt() and 0xFF) shl 8) or
|
||||||
|
((this[offset + 2].toInt() and 0xFF) shl 16) or
|
||||||
|
((this[offset + 3].toInt() and 0xFF) shl 24)
|
||||||
|
|
||||||
|
private fun Int.intToLittleEndian(
|
||||||
|
output: ByteArray,
|
||||||
|
offset: Int,
|
||||||
|
) {
|
||||||
|
output[offset] = (this and 0xFF).toByte()
|
||||||
|
output[offset + 1] = (this ushr 8 and 0xFF).toByte()
|
||||||
|
output[offset + 2] = (this ushr 16 and 0xFF).toByte()
|
||||||
|
output[offset + 3] = (this ushr 24 and 0xFF).toByte()
|
||||||
|
}
|
||||||
+202
@@ -0,0 +1,202 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.quartz.nip44Encryption.crypto
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pure Kotlin implementation of Poly1305 one-time authenticator (RFC 8439 §2.5).
|
||||||
|
*
|
||||||
|
* Uses 130-bit arithmetic with 5 limbs of 26 bits each, following the approach
|
||||||
|
* from D.J. Bernstein's reference implementation. All functions are stateless
|
||||||
|
* and thread-safe.
|
||||||
|
*
|
||||||
|
* The 32-byte key is split into:
|
||||||
|
* - r (first 16 bytes): clamped per spec, used as the multiplier
|
||||||
|
* - s (last 16 bytes): added at the end
|
||||||
|
*/
|
||||||
|
object Poly1305 {
|
||||||
|
/**
|
||||||
|
* Compute a 16-byte Poly1305 MAC.
|
||||||
|
*
|
||||||
|
* @param message the data to authenticate
|
||||||
|
* @param key 32-byte one-time key (r || s)
|
||||||
|
* @return 16-byte authentication tag
|
||||||
|
*/
|
||||||
|
fun mac(
|
||||||
|
message: ByteArray,
|
||||||
|
key: ByteArray,
|
||||||
|
): ByteArray {
|
||||||
|
// Parse and clamp r from first 16 bytes of key
|
||||||
|
val r0 = (key.leToLong(0)) and 0x3ffffff
|
||||||
|
val r1 = (key.leToLong(3) ushr 2) and 0x3ffff03
|
||||||
|
val r2 = (key.leToLong(6) ushr 4) and 0x3ffc0ff
|
||||||
|
val r3 = (key.leToLong(9) ushr 6) and 0x3f03fff
|
||||||
|
val r4 = (key.leToLong(12) ushr 8) and 0x00fffff
|
||||||
|
|
||||||
|
// Precompute 5*r for the reduction step
|
||||||
|
val s1 = r1 * 5
|
||||||
|
val s2 = r2 * 5
|
||||||
|
val s3 = r3 * 5
|
||||||
|
val s4 = r4 * 5
|
||||||
|
|
||||||
|
// Accumulator: 5 limbs of 26 bits
|
||||||
|
var h0 = 0L
|
||||||
|
var h1 = 0L
|
||||||
|
var h2 = 0L
|
||||||
|
var h3 = 0L
|
||||||
|
var h4 = 0L
|
||||||
|
|
||||||
|
val fullBlocks = message.size / 16
|
||||||
|
val remainder = message.size % 16
|
||||||
|
|
||||||
|
// Process full 16-byte blocks
|
||||||
|
for (i in 0 until fullBlocks) {
|
||||||
|
val offset = i * 16
|
||||||
|
h0 += (message.leToLong(offset)) and 0x3ffffff
|
||||||
|
h1 += (message.leToLong(offset + 3) ushr 2) and 0x3ffffff
|
||||||
|
h2 += (message.leToLong(offset + 6) ushr 4) and 0x3ffffff
|
||||||
|
h3 += (message.leToLong(offset + 9) ushr 6) and 0x3ffffff
|
||||||
|
h4 += (message.leToLong(offset + 12) ushr 8) or (1L shl 24) // hibit = 1
|
||||||
|
|
||||||
|
// Multiply and reduce
|
||||||
|
val d0 = h0 * r0 + h1 * s4 + h2 * s3 + h3 * s2 + h4 * s1
|
||||||
|
val d1 = h0 * r1 + h1 * r0 + h2 * s4 + h3 * s3 + h4 * s2
|
||||||
|
val d2 = h0 * r2 + h1 * r1 + h2 * r0 + h3 * s4 + h4 * s3
|
||||||
|
val d3 = h0 * r3 + h1 * r2 + h2 * r1 + h3 * r0 + h4 * s4
|
||||||
|
val d4 = h0 * r4 + h1 * r3 + h2 * r2 + h3 * r1 + h4 * r0
|
||||||
|
|
||||||
|
// Partial reduction mod 2^130 - 5
|
||||||
|
var c: Long
|
||||||
|
c = d0 ushr 26; h0 = d0 and 0x3ffffff; h1 = d1 + c
|
||||||
|
c = h1 ushr 26; h1 = h1 and 0x3ffffff; h2 = d2 + c
|
||||||
|
c = h2 ushr 26; h2 = h2 and 0x3ffffff; h3 = d3 + c
|
||||||
|
c = h3 ushr 26; h3 = h3 and 0x3ffffff; h4 = d4 + c
|
||||||
|
c = h4 ushr 26; h4 = h4 and 0x3ffffff; h0 += c * 5
|
||||||
|
c = h0 ushr 26; h0 = h0 and 0x3ffffff; h1 += c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process remaining bytes (if any)
|
||||||
|
if (remainder > 0) {
|
||||||
|
val block = ByteArray(17)
|
||||||
|
message.copyInto(block, 0, fullBlocks * 16, message.size)
|
||||||
|
block[remainder] = 1 // pad with 0x01
|
||||||
|
|
||||||
|
h0 += (block.leToLong(0)) and 0x3ffffff
|
||||||
|
h1 += (block.leToLong(3) ushr 2) and 0x3ffffff
|
||||||
|
h2 += (block.leToLong(6) ushr 4) and 0x3ffffff
|
||||||
|
h3 += (block.leToLong(9) ushr 6) and 0x3ffffff
|
||||||
|
h4 += (block.leToLong(12) ushr 8)
|
||||||
|
|
||||||
|
val d0 = h0 * r0 + h1 * s4 + h2 * s3 + h3 * s2 + h4 * s1
|
||||||
|
val d1 = h0 * r1 + h1 * r0 + h2 * s4 + h3 * s3 + h4 * s2
|
||||||
|
val d2 = h0 * r2 + h1 * r1 + h2 * r0 + h3 * s4 + h4 * s3
|
||||||
|
val d3 = h0 * r3 + h1 * r2 + h2 * r1 + h3 * r0 + h4 * s4
|
||||||
|
val d4 = h0 * r4 + h1 * r3 + h2 * r2 + h3 * r1 + h4 * r0
|
||||||
|
|
||||||
|
var c: Long
|
||||||
|
c = d0 ushr 26; h0 = d0 and 0x3ffffff; h1 = d1 + c
|
||||||
|
c = h1 ushr 26; h1 = h1 and 0x3ffffff; h2 = d2 + c
|
||||||
|
c = h2 ushr 26; h2 = h2 and 0x3ffffff; h3 = d3 + c
|
||||||
|
c = h3 ushr 26; h3 = h3 and 0x3ffffff; h4 = d4 + c
|
||||||
|
c = h4 ushr 26; h4 = h4 and 0x3ffffff; h0 += c * 5
|
||||||
|
c = h0 ushr 26; h0 = h0 and 0x3ffffff; h1 += c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final reduction: fully carry and reduce mod 2^130 - 5
|
||||||
|
var c: Long
|
||||||
|
c = h1 ushr 26; h1 = h1 and 0x3ffffff; h2 += c
|
||||||
|
c = h2 ushr 26; h2 = h2 and 0x3ffffff; h3 += c
|
||||||
|
c = h3 ushr 26; h3 = h3 and 0x3ffffff; h4 += c
|
||||||
|
c = h4 ushr 26; h4 = h4 and 0x3ffffff; h0 += c * 5
|
||||||
|
c = h0 ushr 26; h0 = h0 and 0x3ffffff; h1 += c
|
||||||
|
|
||||||
|
// Compute h + -(2^130 - 5) = h - 2^130 + 5
|
||||||
|
var g0 = h0 + 5; c = g0 ushr 26; g0 = g0 and 0x3ffffff
|
||||||
|
var g1 = h1 + c; c = g1 ushr 26; g1 = g1 and 0x3ffffff
|
||||||
|
var g2 = h2 + c; c = g2 ushr 26; g2 = g2 and 0x3ffffff
|
||||||
|
var g3 = h3 + c; c = g3 ushr 26; g3 = g3 and 0x3ffffff
|
||||||
|
var g4 = h4 + c - (1L shl 26)
|
||||||
|
|
||||||
|
// Select h if g4 is negative (bit 63 set), else select g
|
||||||
|
val mask = (g4 ushr 63) - 1 // 0 if h, all-ones if g
|
||||||
|
g0 = g0 and mask
|
||||||
|
g1 = g1 and mask
|
||||||
|
g2 = g2 and mask
|
||||||
|
g3 = g3 and mask
|
||||||
|
g4 = g4 and mask
|
||||||
|
val nmask = mask.inv()
|
||||||
|
h0 = (h0 and nmask) or g0
|
||||||
|
h1 = (h1 and nmask) or g1
|
||||||
|
h2 = (h2 and nmask) or g2
|
||||||
|
h3 = (h3 and nmask) or g3
|
||||||
|
h4 = (h4 and nmask) or g4
|
||||||
|
|
||||||
|
// Assemble h into 4 32-bit words
|
||||||
|
var f0 = ((h0) or (h1 shl 26)) and 0xffffffffL
|
||||||
|
var f1 = ((h1 ushr 6) or (h2 shl 20)) and 0xffffffffL
|
||||||
|
var f2 = ((h2 ushr 12) or (h3 shl 14)) and 0xffffffffL
|
||||||
|
var f3 = ((h3 ushr 18) or (h4 shl 8)) and 0xffffffffL
|
||||||
|
|
||||||
|
// Add s (second half of the key)
|
||||||
|
val s0 = key.leToUInt(16)
|
||||||
|
val s1Long = key.leToUInt(20)
|
||||||
|
val s2Long = key.leToUInt(24)
|
||||||
|
val s3Long = key.leToUInt(28)
|
||||||
|
|
||||||
|
f0 += s0; c = f0 ushr 32
|
||||||
|
f1 += s1Long + c; c = f1 ushr 32
|
||||||
|
f2 += s2Long + c; c = f2 ushr 32
|
||||||
|
f3 += s3Long + c
|
||||||
|
|
||||||
|
// Output as little-endian bytes
|
||||||
|
val tag = ByteArray(16)
|
||||||
|
tag[0] = (f0).toByte()
|
||||||
|
tag[1] = (f0 ushr 8).toByte()
|
||||||
|
tag[2] = (f0 ushr 16).toByte()
|
||||||
|
tag[3] = (f0 ushr 24).toByte()
|
||||||
|
tag[4] = (f1).toByte()
|
||||||
|
tag[5] = (f1 ushr 8).toByte()
|
||||||
|
tag[6] = (f1 ushr 16).toByte()
|
||||||
|
tag[7] = (f1 ushr 24).toByte()
|
||||||
|
tag[8] = (f2).toByte()
|
||||||
|
tag[9] = (f2 ushr 8).toByte()
|
||||||
|
tag[10] = (f2 ushr 16).toByte()
|
||||||
|
tag[11] = (f2 ushr 24).toByte()
|
||||||
|
tag[12] = (f3).toByte()
|
||||||
|
tag[13] = (f3 ushr 8).toByte()
|
||||||
|
tag[14] = (f3 ushr 16).toByte()
|
||||||
|
tag[15] = (f3 ushr 24).toByte()
|
||||||
|
return tag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Read 4 bytes as unsigned little-endian Long starting at [offset]. */
|
||||||
|
private fun ByteArray.leToUInt(offset: Int): Long =
|
||||||
|
(this[offset].toLong() and 0xFF) or
|
||||||
|
((this[offset + 1].toLong() and 0xFF) shl 8) or
|
||||||
|
((this[offset + 2].toLong() and 0xFF) shl 16) or
|
||||||
|
((this[offset + 3].toLong() and 0xFF) shl 24)
|
||||||
|
|
||||||
|
/** Read 4 bytes as unsigned little-endian Long starting at [offset]. Used for Poly1305 limb loading. */
|
||||||
|
private fun ByteArray.leToLong(offset: Int): Long =
|
||||||
|
(this[offset].toLong() and 0xFF) or
|
||||||
|
((this[offset + 1].toLong() and 0xFF) shl 8) or
|
||||||
|
((this[offset + 2].toLong() and 0xFF) shl 16) or
|
||||||
|
((this[offset + 3].toLong() and 0xFF) shl 24)
|
||||||
+168
@@ -0,0 +1,168 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.quartz.nip44Encryption.crypto
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pure Kotlin implementation of XChaCha20-Poly1305 AEAD (RFC 8439 §2.8 + XChaCha20 draft).
|
||||||
|
*
|
||||||
|
* All functions are stateless and thread-safe.
|
||||||
|
*
|
||||||
|
* Construction:
|
||||||
|
* 1. Derive subkey via HChaCha20(key, nonce[0..15])
|
||||||
|
* 2. Build 12-byte subnonce: 0x00000000 || nonce[16..23]
|
||||||
|
* 3. Generate Poly1305 OTK from ChaCha20 block 0
|
||||||
|
* 4. Encrypt with ChaCha20 starting at counter 1
|
||||||
|
* 5. Compute Poly1305 tag over: pad16(ad) || pad16(ciphertext) || len(ad) || len(ct)
|
||||||
|
*/
|
||||||
|
object XChaCha20Poly1305 {
|
||||||
|
private const val TAG_SIZE = 16
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AEAD encrypt.
|
||||||
|
*
|
||||||
|
* @param plaintext message to encrypt
|
||||||
|
* @param ad associated data (authenticated but not encrypted)
|
||||||
|
* @param nonce 24-byte nonce
|
||||||
|
* @param key 32-byte key
|
||||||
|
* @return ciphertext || 16-byte tag
|
||||||
|
*/
|
||||||
|
fun encrypt(
|
||||||
|
plaintext: ByteArray,
|
||||||
|
ad: ByteArray,
|
||||||
|
nonce: ByteArray,
|
||||||
|
key: ByteArray,
|
||||||
|
): ByteArray {
|
||||||
|
// Step 1-2: Derive subkey and subnonce
|
||||||
|
val subKey = ChaCha20Core.hChaCha20(key, nonce.copyOfRange(0, 16))
|
||||||
|
val subNonce = ByteArray(12)
|
||||||
|
nonce.copyInto(subNonce, destinationOffset = 4, startIndex = 16, endIndex = 24)
|
||||||
|
|
||||||
|
// Step 3: Generate Poly1305 one-time key from block 0
|
||||||
|
val polyKey = ChaCha20Core.chaCha20Block(subKey, 0, subNonce).copyOfRange(0, 32)
|
||||||
|
|
||||||
|
// Step 4: Encrypt plaintext with counter starting at 1
|
||||||
|
val ciphertext = ChaCha20Core.chaCha20Xor(plaintext, subKey, subNonce, counter = 1)
|
||||||
|
|
||||||
|
// Step 5: Compute tag
|
||||||
|
val tag = computeTag(polyKey, ad, ciphertext)
|
||||||
|
|
||||||
|
// Step 6: Return ciphertext || tag
|
||||||
|
return ciphertext + tag
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AEAD decrypt.
|
||||||
|
*
|
||||||
|
* @param ciphertextWithTag ciphertext || 16-byte tag
|
||||||
|
* @param ad associated data
|
||||||
|
* @param nonce 24-byte nonce
|
||||||
|
* @param key 32-byte key
|
||||||
|
* @return plaintext, or throws on authentication failure
|
||||||
|
*/
|
||||||
|
fun decrypt(
|
||||||
|
ciphertextWithTag: ByteArray,
|
||||||
|
ad: ByteArray,
|
||||||
|
nonce: ByteArray,
|
||||||
|
key: ByteArray,
|
||||||
|
): ByteArray {
|
||||||
|
require(ciphertextWithTag.size >= TAG_SIZE) { "Ciphertext too short" }
|
||||||
|
|
||||||
|
val ctLen = ciphertextWithTag.size - TAG_SIZE
|
||||||
|
val ciphertext = ciphertextWithTag.copyOfRange(0, ctLen)
|
||||||
|
val receivedTag = ciphertextWithTag.copyOfRange(ctLen, ciphertextWithTag.size)
|
||||||
|
|
||||||
|
// Step 1-2: Derive subkey and subnonce
|
||||||
|
val subKey = ChaCha20Core.hChaCha20(key, nonce.copyOfRange(0, 16))
|
||||||
|
val subNonce = ByteArray(12)
|
||||||
|
nonce.copyInto(subNonce, destinationOffset = 4, startIndex = 16, endIndex = 24)
|
||||||
|
|
||||||
|
// Step 3: Generate Poly1305 one-time key
|
||||||
|
val polyKey = ChaCha20Core.chaCha20Block(subKey, 0, subNonce).copyOfRange(0, 32)
|
||||||
|
|
||||||
|
// Step 4: Verify tag
|
||||||
|
val expectedTag = computeTag(polyKey, ad, ciphertext)
|
||||||
|
check(constantTimeEquals(receivedTag, expectedTag)) { "Authentication failed" }
|
||||||
|
|
||||||
|
// Step 5: Decrypt
|
||||||
|
return ChaCha20Core.chaCha20Xor(ciphertext, subKey, subNonce, counter = 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute Poly1305 tag over the AEAD construction:
|
||||||
|
* pad16(ad) || pad16(ciphertext) || len(ad) as 8-byte LE || len(ct) as 8-byte LE
|
||||||
|
*/
|
||||||
|
private fun computeTag(
|
||||||
|
polyKey: ByteArray,
|
||||||
|
ad: ByteArray,
|
||||||
|
ciphertext: ByteArray,
|
||||||
|
): ByteArray {
|
||||||
|
val adPadLen = if (ad.size % 16 == 0) 0 else 16 - (ad.size % 16)
|
||||||
|
val ctPadLen = if (ciphertext.size % 16 == 0) 0 else 16 - (ciphertext.size % 16)
|
||||||
|
|
||||||
|
val macData = ByteArray(ad.size + adPadLen + ciphertext.size + ctPadLen + 16)
|
||||||
|
var offset = 0
|
||||||
|
|
||||||
|
// pad16(ad)
|
||||||
|
ad.copyInto(macData, offset)
|
||||||
|
offset += ad.size + adPadLen
|
||||||
|
|
||||||
|
// pad16(ciphertext)
|
||||||
|
ciphertext.copyInto(macData, offset)
|
||||||
|
offset += ciphertext.size + ctPadLen
|
||||||
|
|
||||||
|
// len(ad) as 8-byte little-endian
|
||||||
|
longToLittleEndian(ad.size.toLong(), macData, offset)
|
||||||
|
offset += 8
|
||||||
|
|
||||||
|
// len(ciphertext) as 8-byte little-endian
|
||||||
|
longToLittleEndian(ciphertext.size.toLong(), macData, offset)
|
||||||
|
|
||||||
|
return Poly1305.mac(macData, polyKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Constant-time comparison to prevent timing attacks. */
|
||||||
|
private fun constantTimeEquals(
|
||||||
|
a: ByteArray,
|
||||||
|
b: ByteArray,
|
||||||
|
): Boolean {
|
||||||
|
if (a.size != b.size) return false
|
||||||
|
var result = 0
|
||||||
|
for (i in a.indices) {
|
||||||
|
result = result or (a[i].toInt() xor b[i].toInt())
|
||||||
|
}
|
||||||
|
return result == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun longToLittleEndian(
|
||||||
|
value: Long,
|
||||||
|
output: ByteArray,
|
||||||
|
offset: Int,
|
||||||
|
) {
|
||||||
|
output[offset] = (value and 0xFF).toByte()
|
||||||
|
output[offset + 1] = (value ushr 8 and 0xFF).toByte()
|
||||||
|
output[offset + 2] = (value ushr 16 and 0xFF).toByte()
|
||||||
|
output[offset + 3] = (value ushr 24 and 0xFF).toByte()
|
||||||
|
output[offset + 4] = (value ushr 32 and 0xFF).toByte()
|
||||||
|
output[offset + 5] = (value ushr 40 and 0xFF).toByte()
|
||||||
|
output[offset + 6] = (value ushr 48 and 0xFF).toByte()
|
||||||
|
output[offset + 7] = (value ushr 56 and 0xFF).toByte()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,7 +20,14 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.quartz.utils
|
package com.vitorpamplona.quartz.utils
|
||||||
|
|
||||||
expect object LibSodiumInstance {
|
import com.vitorpamplona.quartz.nip44Encryption.crypto.ChaCha20Core
|
||||||
|
import com.vitorpamplona.quartz.nip44Encryption.crypto.XChaCha20Poly1305
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pure Kotlin replacement for LazySodium/libsodium.
|
||||||
|
* All functions are stateless and thread-safe.
|
||||||
|
*/
|
||||||
|
object LibSodiumInstance {
|
||||||
fun cryptoAeadXChaCha20Poly1305IetfDecrypt(
|
fun cryptoAeadXChaCha20Poly1305IetfDecrypt(
|
||||||
message: ByteArray,
|
message: ByteArray,
|
||||||
nSec: ByteArray,
|
nSec: ByteArray,
|
||||||
@@ -28,7 +35,14 @@ expect object LibSodiumInstance {
|
|||||||
ad: ByteArray,
|
ad: ByteArray,
|
||||||
nPub: ByteArray,
|
nPub: ByteArray,
|
||||||
k: ByteArray,
|
k: ByteArray,
|
||||||
): Boolean
|
): Boolean =
|
||||||
|
try {
|
||||||
|
val plaintext = XChaCha20Poly1305.decrypt(ciphertext, ad, nPub, k)
|
||||||
|
plaintext.copyInto(message)
|
||||||
|
true
|
||||||
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
fun cryptoAeadXChaCha20Poly1305IetfEncrypt(
|
fun cryptoAeadXChaCha20Poly1305IetfEncrypt(
|
||||||
ciphertext: ByteArray,
|
ciphertext: ByteArray,
|
||||||
@@ -37,18 +51,24 @@ expect object LibSodiumInstance {
|
|||||||
nSec: ByteArray,
|
nSec: ByteArray,
|
||||||
nPub: ByteArray,
|
nPub: ByteArray,
|
||||||
k: ByteArray,
|
k: ByteArray,
|
||||||
): Boolean
|
): Boolean =
|
||||||
|
try {
|
||||||
|
val result = XChaCha20Poly1305.encrypt(message, ad, nPub, k)
|
||||||
|
result.copyInto(ciphertext)
|
||||||
|
true
|
||||||
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
fun cryptoStreamChaCha20IetfXor(
|
fun cryptoStreamChaCha20IetfXor(
|
||||||
message: ByteArray,
|
message: ByteArray,
|
||||||
nonce: ByteArray?,
|
nonce: ByteArray?,
|
||||||
key: ByteArray?,
|
key: ByteArray?,
|
||||||
): ByteArray
|
): ByteArray = ChaCha20Core.chaCha20Xor(message, key!!, nonce!!, counter = 0)
|
||||||
|
|
||||||
// This function wasn't available in the bindings library. I had to move them here from C
|
|
||||||
fun cryptoStreamXChaCha20Xor(
|
fun cryptoStreamXChaCha20Xor(
|
||||||
messageBytes: ByteArray,
|
messageBytes: ByteArray,
|
||||||
nonce: ByteArray,
|
nonce: ByteArray,
|
||||||
key: ByteArray,
|
key: ByteArray,
|
||||||
): ByteArray
|
): ByteArray = ChaCha20Core.xChaCha20Xor(messageBytes, nonce, key)
|
||||||
}
|
}
|
||||||
|
|||||||
+253
@@ -0,0 +1,253 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.quartz.nip44Encryption.crypto
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertContentEquals
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test vectors from RFC 8439, XChaCha20 draft (draft-irtf-cfrg-xchacha-03),
|
||||||
|
* and libsodium test suite.
|
||||||
|
*/
|
||||||
|
class ChaCha20CoreTest {
|
||||||
|
private fun hex(s: String): ByteArray =
|
||||||
|
s.replace(" ", "").chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
||||||
|
|
||||||
|
private fun ByteArray.toHex(): String = joinToString("") { "%02x".format(it) }
|
||||||
|
|
||||||
|
// ===== RFC 8439 §2.3.2: ChaCha20 Block Function Test Vector =====
|
||||||
|
@Test
|
||||||
|
fun testChaCha20Block_RFC8439() {
|
||||||
|
val key = hex("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f")
|
||||||
|
val nonce = hex("000000090000004a00000000")
|
||||||
|
val counter = 1
|
||||||
|
|
||||||
|
val block = ChaCha20Core.chaCha20Block(key, counter, nonce)
|
||||||
|
|
||||||
|
val expected =
|
||||||
|
hex(
|
||||||
|
"10f1e7e4d13b5915500fdd1fa32071c4" +
|
||||||
|
"c7d1f4c733c068030422aa9ac3d46c4e" +
|
||||||
|
"d2826446079faa0914c2d705d98b02a2" +
|
||||||
|
"b5129cd1de164eb9cbd083e8a2503c4e",
|
||||||
|
)
|
||||||
|
assertContentEquals(expected, block)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== RFC 8439 §2.4.2: ChaCha20 Encryption Test Vector =====
|
||||||
|
@Test
|
||||||
|
fun testChaCha20Encrypt_RFC8439() {
|
||||||
|
val key = hex("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f")
|
||||||
|
val nonce = hex("000000000000004a00000000")
|
||||||
|
val counter = 1
|
||||||
|
val plaintext =
|
||||||
|
"Ladies and Gentlemen of the class of '99: If I could offer you only one tip for the future, sunscreen would be it."
|
||||||
|
.encodeToByteArray()
|
||||||
|
|
||||||
|
val ciphertext = ChaCha20Core.chaCha20Xor(plaintext, key, nonce, counter)
|
||||||
|
|
||||||
|
val expected =
|
||||||
|
hex(
|
||||||
|
"6e2e359a2568f98041ba0728dd0d6981" +
|
||||||
|
"e97e7aec1d4360c20a27afccfd9fae0b" +
|
||||||
|
"f91b65c5524733ab8f593dabcd62b357" +
|
||||||
|
"1639d624e65152ab8f530c359f0861d8" +
|
||||||
|
"07ca0dbf500d6a6156a38e088a22b65e" +
|
||||||
|
"52bc514d16ccf806818ce91ab7793736" +
|
||||||
|
"5af90bbf74a35be6b40b8eedf2785e42" +
|
||||||
|
"874d",
|
||||||
|
)
|
||||||
|
assertContentEquals(expected, ciphertext)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== RFC 8439 §2.5.2: Poly1305 Test Vector =====
|
||||||
|
@Test
|
||||||
|
fun testPoly1305_RFC8439() {
|
||||||
|
val key = hex("85d6be7857556d337f4452fe42d506a80103808afb0db2fd4abff6af4149f51b")
|
||||||
|
val message =
|
||||||
|
"Cryptographic Forum Research Group".encodeToByteArray()
|
||||||
|
|
||||||
|
val tag = Poly1305.mac(message, key)
|
||||||
|
|
||||||
|
val expected = hex("a8061dc1305136c6c22b8baf0c0127a9")
|
||||||
|
assertContentEquals(expected, tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== RFC 8439 §2.8.2: AEAD ChaCha20-Poly1305 Test Vector =====
|
||||||
|
// Adapted for XChaCha20 via the XChaCha20 draft test vector
|
||||||
|
@Test
|
||||||
|
fun testPoly1305KeyGeneration_RFC8439() {
|
||||||
|
// RFC 8439 §2.6.2 Poly1305 Key Generation
|
||||||
|
val key = hex("808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f")
|
||||||
|
val nonce = hex("000000000001020304050607")
|
||||||
|
|
||||||
|
val block = ChaCha20Core.chaCha20Block(key, 0, nonce)
|
||||||
|
val polyKey = block.copyOfRange(0, 32)
|
||||||
|
|
||||||
|
val expected = hex("8ad5a08b905f81cc815040274ab29471a833b637e3fd0da508dbb8e2fdd1a646")
|
||||||
|
assertContentEquals(expected, polyKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== XChaCha20 draft §2.2.1: HChaCha20 Test Vector =====
|
||||||
|
@Test
|
||||||
|
fun testHChaCha20_Draft() {
|
||||||
|
val key = hex("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f")
|
||||||
|
val input = hex("000000090000004a0000000031415927")
|
||||||
|
|
||||||
|
val subKey = ChaCha20Core.hChaCha20(key, input)
|
||||||
|
|
||||||
|
val expected = hex("82413b4227b27bfed30e42508a877d73a0f9e4d58a74a853c12ec41326d3ecdc")
|
||||||
|
assertContentEquals(expected, subKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== XChaCha20 draft §A.3.1: XChaCha20-Poly1305 AEAD Test Vector =====
|
||||||
|
@Test
|
||||||
|
fun testXChaCha20Poly1305Encrypt_Draft() {
|
||||||
|
val key = hex("808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f")
|
||||||
|
val nonce = hex("404142434445464748494a4b4c4d4e4f5051525354555657")
|
||||||
|
val ad = hex("50515253c0c1c2c3c4c5c6c7")
|
||||||
|
val plaintext =
|
||||||
|
"Ladies and Gentlemen of the class of '99: If I could offer you only one tip for the future, sunscreen would be it."
|
||||||
|
.encodeToByteArray()
|
||||||
|
|
||||||
|
val result = XChaCha20Poly1305.encrypt(plaintext, ad, nonce, key)
|
||||||
|
|
||||||
|
// Ciphertext portion
|
||||||
|
val expectedCiphertext =
|
||||||
|
hex(
|
||||||
|
"bd6d179d3e83d43b9576579493c0e939572a1700252bfaccbed2902c21396cbb" +
|
||||||
|
"731c7f1b0b4aa6440bf3a82f4eda7e39ae64c6708c54c216cb96b72e1213b452" +
|
||||||
|
"2f8c9ba40db5d945b11b69b982c1bb9e3f3fac2bc369488f76b2383565d3fff9" +
|
||||||
|
"21f9664c97637da9768812f615c68b13b52e",
|
||||||
|
)
|
||||||
|
// Tag portion
|
||||||
|
val expectedTag = hex("c0875924c1c7987947deafd8780acf49")
|
||||||
|
|
||||||
|
val ciphertext = result.copyOfRange(0, result.size - 16)
|
||||||
|
val tag = result.copyOfRange(result.size - 16, result.size)
|
||||||
|
|
||||||
|
assertContentEquals(expectedCiphertext, ciphertext)
|
||||||
|
assertContentEquals(expectedTag, tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testXChaCha20Poly1305Decrypt_Draft() {
|
||||||
|
val key = hex("808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f")
|
||||||
|
val nonce = hex("404142434445464748494a4b4c4d4e4f5051525354555657")
|
||||||
|
val ad = hex("50515253c0c1c2c3c4c5c6c7")
|
||||||
|
val ciphertextWithTag =
|
||||||
|
hex(
|
||||||
|
"bd6d179d3e83d43b9576579493c0e939572a1700252bfaccbed2902c21396cbb" +
|
||||||
|
"731c7f1b0b4aa6440bf3a82f4eda7e39ae64c6708c54c216cb96b72e1213b452" +
|
||||||
|
"2f8c9ba40db5d945b11b69b982c1bb9e3f3fac2bc369488f76b2383565d3fff9" +
|
||||||
|
"21f9664c97637da9768812f615c68b13b52e" +
|
||||||
|
"c0875924c1c7987947deafd8780acf49",
|
||||||
|
)
|
||||||
|
|
||||||
|
val plaintext = XChaCha20Poly1305.decrypt(ciphertextWithTag, ad, nonce, key)
|
||||||
|
|
||||||
|
val expected =
|
||||||
|
"Ladies and Gentlemen of the class of '99: If I could offer you only one tip for the future, sunscreen would be it."
|
||||||
|
assertEquals(expected, plaintext.decodeToString())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testXChaCha20Poly1305_TamperedCiphertext() {
|
||||||
|
val key = hex("808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f")
|
||||||
|
val nonce = hex("404142434445464748494a4b4c4d4e4f5051525354555657")
|
||||||
|
val ad = hex("50515253c0c1c2c3c4c5c6c7")
|
||||||
|
val plaintext = "Test message".encodeToByteArray()
|
||||||
|
|
||||||
|
val encrypted = XChaCha20Poly1305.encrypt(plaintext, ad, nonce, key)
|
||||||
|
// Flip a bit in the ciphertext
|
||||||
|
encrypted[0] = (encrypted[0].toInt() xor 1).toByte()
|
||||||
|
|
||||||
|
assertFailsWith<IllegalStateException> {
|
||||||
|
XChaCha20Poly1305.decrypt(encrypted, ad, nonce, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testXChaCha20Poly1305_RoundTrip() {
|
||||||
|
val key = hex("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f")
|
||||||
|
val nonce = hex("000102030405060708090a0b0c0d0e0f1011121314151617")
|
||||||
|
val ad = hex("feedfacedeadbeef")
|
||||||
|
val plaintext = "Hello, Nostr! This is a round-trip test.".encodeToByteArray()
|
||||||
|
|
||||||
|
val encrypted = XChaCha20Poly1305.encrypt(plaintext, ad, nonce, key)
|
||||||
|
val decrypted = XChaCha20Poly1305.decrypt(encrypted, ad, nonce, key)
|
||||||
|
|
||||||
|
assertContentEquals(plaintext, decrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testXChaCha20Poly1305_EmptyPlaintext() {
|
||||||
|
val key = hex("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f")
|
||||||
|
val nonce = hex("000102030405060708090a0b0c0d0e0f1011121314151617")
|
||||||
|
val ad = hex("feedface")
|
||||||
|
val plaintext = ByteArray(0)
|
||||||
|
|
||||||
|
val encrypted = XChaCha20Poly1305.encrypt(plaintext, ad, nonce, key)
|
||||||
|
assertEquals(16, encrypted.size) // Just the tag
|
||||||
|
val decrypted = XChaCha20Poly1305.decrypt(encrypted, ad, nonce, key)
|
||||||
|
assertContentEquals(plaintext, decrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testXChaCha20Poly1305_EmptyAD() {
|
||||||
|
val key = hex("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f")
|
||||||
|
val nonce = hex("000102030405060708090a0b0c0d0e0f1011121314151617")
|
||||||
|
val ad = ByteArray(0)
|
||||||
|
val plaintext = "No additional data".encodeToByteArray()
|
||||||
|
|
||||||
|
val encrypted = XChaCha20Poly1305.encrypt(plaintext, ad, nonce, key)
|
||||||
|
val decrypted = XChaCha20Poly1305.decrypt(encrypted, ad, nonce, key)
|
||||||
|
assertContentEquals(plaintext, decrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== ChaCha20 stream cipher (counter=0) for NIP-44v2 compatibility =====
|
||||||
|
@Test
|
||||||
|
fun testChaCha20Xor_SymmetricEncryptDecrypt() {
|
||||||
|
val key = hex("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f")
|
||||||
|
val nonce = hex("000000090000004a00000000")
|
||||||
|
val plaintext = "Symmetric cipher test".encodeToByteArray()
|
||||||
|
|
||||||
|
val encrypted = ChaCha20Core.chaCha20Xor(plaintext, key, nonce, counter = 0)
|
||||||
|
val decrypted = ChaCha20Core.chaCha20Xor(encrypted, key, nonce, counter = 0)
|
||||||
|
|
||||||
|
assertContentEquals(plaintext, decrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== XChaCha20 stream cipher symmetry =====
|
||||||
|
@Test
|
||||||
|
fun testXChaCha20Xor_SymmetricEncryptDecrypt() {
|
||||||
|
val key = hex("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f")
|
||||||
|
val nonce = hex("000102030405060708090a0b0c0d0e0f1011121314151617")
|
||||||
|
val plaintext = "XChaCha20 symmetric test".encodeToByteArray()
|
||||||
|
|
||||||
|
val encrypted = ChaCha20Core.xChaCha20Xor(plaintext, nonce, key)
|
||||||
|
val decrypted = ChaCha20Core.xChaCha20Xor(encrypted, nonce, key)
|
||||||
|
|
||||||
|
assertContentEquals(plaintext, decrypted)
|
||||||
|
}
|
||||||
|
}
|
||||||
+110
@@ -0,0 +1,110 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.quartz.nip44Encryption.crypto
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertContentEquals
|
||||||
|
|
||||||
|
/** Test vectors from libsodium xchacha20.c */
|
||||||
|
class XChaCha20Test {
|
||||||
|
private fun hex(s: String): ByteArray =
|
||||||
|
s.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
||||||
|
|
||||||
|
// HChaCha20 test vectors from libsodium
|
||||||
|
data class HChaCha20TV(val key: String, val input: String, val expected: String)
|
||||||
|
|
||||||
|
private val hChaCha20Vectors = listOf(
|
||||||
|
HChaCha20TV(
|
||||||
|
"24f11cce8a1b3d61e441561a696c1c1b7e173d084fd4812425435a8896a013dc",
|
||||||
|
"d9660c5900ae19ddad28d6e06e45fe5e",
|
||||||
|
"5966b3eec3bff1189f831f06afe4d4e3be97fa9235ec8c20d08acfbbb4e851e3",
|
||||||
|
),
|
||||||
|
HChaCha20TV(
|
||||||
|
"80a5f6272031e18bb9bcd84f3385da65e7731b7039f13f5e3d475364cd4d42f7",
|
||||||
|
"c0eccc384b44c88e92c57eb2d5ca4dfa",
|
||||||
|
"6ed11741f724009a640a44fce7320954c46e18e0d7ae063bdbc8d7cf372709df",
|
||||||
|
),
|
||||||
|
HChaCha20TV(
|
||||||
|
"cb1fc686c0eec11a89438b6f4013bf110e7171dace3297f3a657a309b3199629",
|
||||||
|
"fcd49b93e5f8f299227e64d40dc864a3",
|
||||||
|
"84b7e96937a1a0a406bb7162eeaad34308d49de60fd2f7ec9dc6a79cbab2ca34",
|
||||||
|
),
|
||||||
|
HChaCha20TV(
|
||||||
|
"6640f4d80af5496ca1bc2cfff1fefbe99638dbceaabd7d0ade118999d45f053d",
|
||||||
|
"31f59ceeeafdbfe8cae7914caeba90d6",
|
||||||
|
"9af4697d2f5574a44834a2c2ae1a0505af9f5d869dbe381a994a18eb374c36a0",
|
||||||
|
),
|
||||||
|
HChaCha20TV(
|
||||||
|
"0693ff36d971225a44ac92c092c60b399e672e4cc5aafd5e31426f123787ac27",
|
||||||
|
"3a6293da061da405db45be1731d5fc4d",
|
||||||
|
"f87b38609142c01095bfc425573bb3c698f9ae866b7e4216840b9c4caf3b0865",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testHChaCha20_LibsodiumVectors() {
|
||||||
|
for ((i, tv) in hChaCha20Vectors.withIndex()) {
|
||||||
|
val result = ChaCha20Core.hChaCha20(hex(tv.key), hex(tv.input))
|
||||||
|
assertContentEquals(hex(tv.expected), result, "HChaCha20 vector $i failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// XChaCha20 stream test vectors from libsodium
|
||||||
|
data class XChaCha20TV(val key: String, val nonce: String, val expected: String)
|
||||||
|
|
||||||
|
private val xChaCha20Vectors = listOf(
|
||||||
|
XChaCha20TV(
|
||||||
|
"79c99798ac67300bbb2704c95c341e3245f3dcb21761b98e52ff45b24f304fc4",
|
||||||
|
"b33ffd3096479bcfbc9aee49417688a0a2554f8d95389419",
|
||||||
|
"c6e9758160083ac604ef90e712ce6e75d7797590744e0cf060f013739c",
|
||||||
|
),
|
||||||
|
XChaCha20TV(
|
||||||
|
"ddf7784fee099612c40700862189d0397fcc4cc4b3cc02b5456b3a97d1186173",
|
||||||
|
"a9a04491e7bf00c3ca91ac7c2d38a777d88993a7047dfcc4",
|
||||||
|
"2f289d371f6f0abc3cb60d11d9b7b29adf6bc5ad843e8493e928448d",
|
||||||
|
),
|
||||||
|
XChaCha20TV(
|
||||||
|
"3d12800e7b014e88d68a73f0a95b04b435719936feba60473f02a9e61ae60682",
|
||||||
|
"56bed2599eac99fb27ebf4ffcb770a64772dec4d5849ea2d",
|
||||||
|
"a2c3c1406f33c054a92760a8e0666b84f84fa3a618f0",
|
||||||
|
),
|
||||||
|
XChaCha20TV(
|
||||||
|
"eadc0e27f77113b5241f8ca9d6f9a5e7f09eee68d8a5cf30700563bf01060b4e",
|
||||||
|
"a171a4ef3fde7c4794c5b86170dc5a099b478f1b852f7b64",
|
||||||
|
"23839f61795c3cdbcee2c749a92543baeeea3cbb721402aa42e6cae140447575f2916c5d71108e3b13357eaf86f060cb",
|
||||||
|
),
|
||||||
|
XChaCha20TV(
|
||||||
|
"91319c9545c7c804ba6b712e22294c386fe31c4ff3d278827637b959d3dbaab2",
|
||||||
|
"410e854b2a911f174aaf1a56540fc3855851f41c65967a4e",
|
||||||
|
"cbe7d24177119b7fdfa8b06ee04dade4256ba7d35ffda6b89f014e479faef6",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testXChaCha20Xor_LibsodiumVectors() {
|
||||||
|
for ((i, tv) in xChaCha20Vectors.withIndex()) {
|
||||||
|
// XOR zeros with keystream to get the expected keystream output
|
||||||
|
val zeros = ByteArray(hex(tv.expected).size)
|
||||||
|
val result = ChaCha20Core.xChaCha20Xor(zeros, hex(tv.nonce), hex(tv.key))
|
||||||
|
assertContentEquals(hex(tv.expected), result, "XChaCha20 vector $i failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025 Vitor Pamplona
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
* this software and associated documentation files (the "Software"), to deal in
|
|
||||||
* the Software without restriction, including without limitation the rights to use,
|
|
||||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
|
||||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
|
||||||
* subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
||||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
||||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
|
||||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
||||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
package com.vitorpamplona.quartz.utils
|
|
||||||
|
|
||||||
import kotlinx.cinterop.CValuesRef
|
|
||||||
import kotlinx.cinterop.ExperimentalForeignApi
|
|
||||||
import kotlinx.cinterop.UByteVar
|
|
||||||
import kotlinx.cinterop.refTo
|
|
||||||
import kotlin.experimental.ExperimentalNativeApi
|
|
||||||
|
|
||||||
actual object LibSodiumInstance {
|
|
||||||
@OptIn(ExperimentalForeignApi::class)
|
|
||||||
actual fun cryptoAeadXChaCha20Poly1305IetfDecrypt(
|
|
||||||
message: ByteArray,
|
|
||||||
nSec: ByteArray,
|
|
||||||
ciphertext: ByteArray,
|
|
||||||
ad: ByteArray,
|
|
||||||
nPub: ByteArray,
|
|
||||||
k: ByteArray,
|
|
||||||
): Boolean {
|
|
||||||
val returnCode =
|
|
||||||
Clibsodium.crypto_aead_xchacha20poly1305_ietf_decrypt(
|
|
||||||
m = message.uRefTo(0),
|
|
||||||
mlen_p = ulongArrayOf(message.size.toULong()).refTo(0),
|
|
||||||
nsec = nSec.uRefTo(0),
|
|
||||||
c = ciphertext.uRefTo(0),
|
|
||||||
clen = ciphertext.size.toULong(),
|
|
||||||
ad = ad.uRefTo(0),
|
|
||||||
adlen = ad.size.toULong(),
|
|
||||||
npub = nPub.uRefTo(0),
|
|
||||||
k = k.uRefTo(0),
|
|
||||||
)
|
|
||||||
return returnCode == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalForeignApi::class)
|
|
||||||
actual fun cryptoAeadXChaCha20Poly1305IetfEncrypt(
|
|
||||||
ciphertext: ByteArray,
|
|
||||||
message: ByteArray,
|
|
||||||
ad: ByteArray,
|
|
||||||
nSec: ByteArray,
|
|
||||||
nPub: ByteArray,
|
|
||||||
k: ByteArray,
|
|
||||||
): Boolean {
|
|
||||||
val retCode =
|
|
||||||
Clibsodium.crypto_aead_xchacha20poly1305_ietf_encrypt(
|
|
||||||
c = ciphertext.uRefTo(0),
|
|
||||||
clen_p = ulongArrayOf(ciphertext.size.toULong()).refTo(0),
|
|
||||||
m = message.uRefTo(0),
|
|
||||||
mlen = message.size.toULong(),
|
|
||||||
ad = ad.uRefTo(0),
|
|
||||||
adlen = ad.size.toULong(),
|
|
||||||
nsec = nSec.uRefTo(0),
|
|
||||||
npub = nPub.uRefTo(0),
|
|
||||||
k = k.uRefTo(0),
|
|
||||||
)
|
|
||||||
|
|
||||||
return retCode == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalForeignApi::class)
|
|
||||||
actual fun cryptoStreamChaCha20IetfXor(
|
|
||||||
message: ByteArray,
|
|
||||||
nonce: ByteArray?,
|
|
||||||
key: ByteArray?,
|
|
||||||
): ByteArray {
|
|
||||||
val ciphertext = ByteArray(message.size)
|
|
||||||
Clibsodium.crypto_stream_chacha20_ietf_xor(
|
|
||||||
c = ciphertext.uRefTo(0),
|
|
||||||
m = message.uRefTo(0),
|
|
||||||
mlen = message.size.toULong(),
|
|
||||||
n = nonce?.uRefTo(0),
|
|
||||||
k = key?.uRefTo(0),
|
|
||||||
)
|
|
||||||
return ciphertext
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class)
|
|
||||||
actual fun cryptoStreamXChaCha20Xor(
|
|
||||||
messageBytes: ByteArray,
|
|
||||||
nonce: ByteArray,
|
|
||||||
key: ByteArray,
|
|
||||||
): ByteArray {
|
|
||||||
val cipher = ByteArray(messageBytes.size)
|
|
||||||
val k2 = ByteArray(32)
|
|
||||||
|
|
||||||
val nonceChaCha = nonce.drop(16).toByteArray()
|
|
||||||
assert(nonceChaCha.size == 8)
|
|
||||||
|
|
||||||
Clibsodium.crypto_core_hchacha20(
|
|
||||||
out = k2.uRefTo(0),
|
|
||||||
nonce.uRefTo(0),
|
|
||||||
k = key.uRefTo(0),
|
|
||||||
c = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
val resultCode =
|
|
||||||
Clibsodium.crypto_stream_chacha20_xor_ic(
|
|
||||||
c = cipher.uRefTo(0),
|
|
||||||
m = messageBytes.uRefTo(0),
|
|
||||||
mlen = messageBytes.size.toULong(),
|
|
||||||
n = nonceChaCha.uRefTo(0),
|
|
||||||
ic = 0L.toULong(),
|
|
||||||
k = k2.uRefTo(0),
|
|
||||||
)
|
|
||||||
return if (resultCode == 0) cipher else throw IllegalStateException("Could not decrypt message")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalForeignApi::class)
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
private fun ByteArray.uRefTo(index: Int) = refTo(index) as CValuesRef<UByteVar>
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025 Vitor Pamplona
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
* this software and associated documentation files (the "Software"), to deal in
|
|
||||||
* the Software without restriction, including without limitation the rights to use,
|
|
||||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
|
||||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
|
||||||
* subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
||||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
||||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
|
||||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
||||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
package com.vitorpamplona.quartz.utils
|
|
||||||
|
|
||||||
import com.goterl.lazysodium.LazySodiumJava
|
|
||||||
import com.goterl.lazysodium.SodiumJava
|
|
||||||
|
|
||||||
actual object LibSodiumInstance {
|
|
||||||
private val libSodium = SodiumJava()
|
|
||||||
private val lazySodium = LazySodiumJava(libSodium)
|
|
||||||
|
|
||||||
actual fun cryptoAeadXChaCha20Poly1305IetfDecrypt(
|
|
||||||
message: ByteArray,
|
|
||||||
nSec: ByteArray,
|
|
||||||
ciphertext: ByteArray,
|
|
||||||
ad: ByteArray,
|
|
||||||
nPub: ByteArray,
|
|
||||||
k: ByteArray,
|
|
||||||
): Boolean =
|
|
||||||
lazySodium.cryptoAeadXChaCha20Poly1305IetfDecrypt(
|
|
||||||
message,
|
|
||||||
longArrayOf(message.size.toLong()),
|
|
||||||
nSec,
|
|
||||||
ciphertext,
|
|
||||||
ciphertext.size.toLong(),
|
|
||||||
ad,
|
|
||||||
ad.size.toLong(),
|
|
||||||
nPub,
|
|
||||||
k,
|
|
||||||
)
|
|
||||||
|
|
||||||
actual fun cryptoAeadXChaCha20Poly1305IetfEncrypt(
|
|
||||||
ciphertext: ByteArray,
|
|
||||||
message: ByteArray,
|
|
||||||
ad: ByteArray,
|
|
||||||
nSec: ByteArray,
|
|
||||||
nPub: ByteArray,
|
|
||||||
k: ByteArray,
|
|
||||||
): Boolean =
|
|
||||||
lazySodium.cryptoAeadXChaCha20Poly1305IetfEncrypt(
|
|
||||||
ciphertext,
|
|
||||||
longArrayOf(ciphertext.size.toLong()),
|
|
||||||
message,
|
|
||||||
message.size.toLong(),
|
|
||||||
ad,
|
|
||||||
ad.size.toLong(),
|
|
||||||
nSec,
|
|
||||||
nPub,
|
|
||||||
k,
|
|
||||||
)
|
|
||||||
|
|
||||||
actual fun cryptoStreamChaCha20IetfXor(
|
|
||||||
message: ByteArray,
|
|
||||||
nonce: ByteArray?,
|
|
||||||
key: ByteArray?,
|
|
||||||
): ByteArray {
|
|
||||||
val ciphertext = ByteArray(message.size)
|
|
||||||
lazySodium.cryptoStreamChaCha20IetfXor(ciphertext, message, message.size.toLong(), nonce, key)
|
|
||||||
return ciphertext
|
|
||||||
}
|
|
||||||
|
|
||||||
actual fun cryptoStreamXChaCha20Xor(
|
|
||||||
messageBytes: ByteArray,
|
|
||||||
nonce: ByteArray,
|
|
||||||
key: ByteArray,
|
|
||||||
): ByteArray {
|
|
||||||
val cipher = ByteArray(messageBytes.size)
|
|
||||||
val k2 = ByteArray(32)
|
|
||||||
|
|
||||||
val nonceChaCha = nonce.drop(16).toByteArray()
|
|
||||||
assert(nonceChaCha.size == 8)
|
|
||||||
|
|
||||||
libSodium.crypto_core_hchacha20(k2, nonce, key, null)
|
|
||||||
val resultCode =
|
|
||||||
libSodium.crypto_stream_chacha20_xor_ic(
|
|
||||||
cipher,
|
|
||||||
messageBytes,
|
|
||||||
messageBytes.size.toLong(),
|
|
||||||
nonceChaCha,
|
|
||||||
0,
|
|
||||||
k2,
|
|
||||||
)
|
|
||||||
|
|
||||||
return if (resultCode == 0) cipher else throw IllegalStateException("Could not decrypt message")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user