diff --git a/meshlink/config/detekt/baseline-androidMain.xml b/meshlink/config/detekt/baseline-androidMain.xml
index a0ec0556..540e6e62 100644
--- a/meshlink/config/detekt/baseline-androidMain.xml
+++ b/meshlink/config/detekt/baseline-androidMain.xml
@@ -66,35 +66,40 @@
MagicNumber:BleTransportAdapterL2capSupport.kt$L2capLink$6
MagicNumber:BleTransportAdapterL2capSupport.kt$L2capLink$8
MagicNumber:BleTransportAdapterScanSupport.kt$6
- MagicNumber:Ed25519Fallback.kt$Ed25519Fallback$0x7fffL
+ MagicNumber:Curve25519FieldArithmetic.kt$Curve25519FieldArithmetic$0x7fffL
+ MagicNumber:Curve25519FieldArithmetic.kt$Curve25519FieldArithmetic$0xFF
+ MagicNumber:Curve25519FieldArithmetic.kt$Curve25519FieldArithmetic$0xffL
+ MagicNumber:Curve25519FieldArithmetic.kt$Curve25519FieldArithmetic$0xffedL
+ MagicNumber:Curve25519FieldArithmetic.kt$Curve25519FieldArithmetic$0xffffL
+ MagicNumber:Curve25519FieldArithmetic.kt$Curve25519FieldArithmetic$10
+ MagicNumber:Curve25519FieldArithmetic.kt$Curve25519FieldArithmetic$100
+ MagicNumber:Curve25519FieldArithmetic.kt$Curve25519FieldArithmetic$14
+ MagicNumber:Curve25519FieldArithmetic.kt$Curve25519FieldArithmetic$15
+ MagicNumber:Curve25519FieldArithmetic.kt$Curve25519FieldArithmetic$16
+ MagicNumber:Curve25519FieldArithmetic.kt$Curve25519FieldArithmetic$20
+ MagicNumber:Curve25519FieldArithmetic.kt$Curve25519FieldArithmetic$3
+ MagicNumber:Curve25519FieldArithmetic.kt$Curve25519FieldArithmetic$37L
+ MagicNumber:Curve25519FieldArithmetic.kt$Curve25519FieldArithmetic$38L
+ MagicNumber:Curve25519FieldArithmetic.kt$Curve25519FieldArithmetic$5
+ MagicNumber:Curve25519FieldArithmetic.kt$Curve25519FieldArithmetic$50
+ MagicNumber:Curve25519FieldArithmetic.kt$Curve25519FieldArithmetic$65535L
+ MagicNumber:Curve25519FieldArithmetic.kt$Curve25519FieldArithmetic$65536L
+ MagicNumber:Curve25519FieldArithmetic.kt$Curve25519FieldArithmetic$8
+ MagicNumber:Curve25519PointArithmetic.kt$Curve25519PointArithmetic$0xFF
+ MagicNumber:Curve25519PointArithmetic.kt$Curve25519PointArithmetic$31
+ MagicNumber:Curve25519PointArithmetic.kt$Curve25519PointArithmetic$4
+ MagicNumber:Curve25519PointArithmetic.kt$Curve25519PointArithmetic$7
MagicNumber:Ed25519Fallback.kt$Ed25519Fallback$0xFF
- MagicNumber:Ed25519Fallback.kt$Ed25519Fallback$0xffL
- MagicNumber:Ed25519Fallback.kt$Ed25519Fallback$0xffedL
- MagicNumber:Ed25519Fallback.kt$Ed25519Fallback$0xffffL
- MagicNumber:Ed25519Fallback.kt$Ed25519Fallback$10
- MagicNumber:Ed25519Fallback.kt$Ed25519Fallback$100
MagicNumber:Ed25519Fallback.kt$Ed25519Fallback$12
MagicNumber:Ed25519Fallback.kt$Ed25519Fallback$127
MagicNumber:Ed25519Fallback.kt$Ed25519Fallback$128L
- MagicNumber:Ed25519Fallback.kt$Ed25519Fallback$14
- MagicNumber:Ed25519Fallback.kt$Ed25519Fallback$15
- MagicNumber:Ed25519Fallback.kt$Ed25519Fallback$16
MagicNumber:Ed25519Fallback.kt$Ed25519Fallback$16L
- MagicNumber:Ed25519Fallback.kt$Ed25519Fallback$20
MagicNumber:Ed25519Fallback.kt$Ed25519Fallback$248
MagicNumber:Ed25519Fallback.kt$Ed25519Fallback$255L
- MagicNumber:Ed25519Fallback.kt$Ed25519Fallback$3
MagicNumber:Ed25519Fallback.kt$Ed25519Fallback$31
- MagicNumber:Ed25519Fallback.kt$Ed25519Fallback$37L
- MagicNumber:Ed25519Fallback.kt$Ed25519Fallback$38L
MagicNumber:Ed25519Fallback.kt$Ed25519Fallback$4
- MagicNumber:Ed25519Fallback.kt$Ed25519Fallback$5
- MagicNumber:Ed25519Fallback.kt$Ed25519Fallback$50
MagicNumber:Ed25519Fallback.kt$Ed25519Fallback$63
MagicNumber:Ed25519Fallback.kt$Ed25519Fallback$64
- MagicNumber:Ed25519Fallback.kt$Ed25519Fallback$65535L
- MagicNumber:Ed25519Fallback.kt$Ed25519Fallback$65536L
- MagicNumber:Ed25519Fallback.kt$Ed25519Fallback$7
MagicNumber:Ed25519Fallback.kt$Ed25519Fallback$8
MagicNumber:GattNotifyClient.kt$GattNotifyClient$6
MagicNumber:GattNotifyClient.kt$GattNotifyClient.<no name provided>$517
@@ -108,11 +113,6 @@
MagicNumber:JcaCapabilityProbe.kt$JcaCapabilityProbe$0x45
MagicNumber:JcaCapabilityProbe.kt$JcaCapabilityProbe$12
MagicNumber:JcaCapabilityProbe.kt$JcaCapabilityProbe$32
- MagicNumber:L2capFrameBuffer.kt$L2capFrameBuffer$0xFF
- MagicNumber:L2capFrameBuffer.kt$L2capFrameBuffer$16
- MagicNumber:L2capFrameBuffer.kt$L2capFrameBuffer$24
- MagicNumber:L2capFrameBuffer.kt$L2capFrameBuffer$3
- MagicNumber:L2capFrameBuffer.kt$L2capFrameBuffer$8
MagicNumber:L2capSendSupport.kt$6
MagicNumber:PreferredGattSendSupport.kt$25
MagicNumber:PreferredGattSendSupport.kt$6
@@ -201,12 +201,13 @@
TooManyFunctions:BleTransportAdapterL2capSupport.kt$ch.trancee.meshlink.platform.android.l2cap.BleTransportAdapterL2capSupport.kt
TooManyFunctions:BleTransportDiscoveryLifecycle.kt$BleTransportDiscoveryLifecycle
TooManyFunctions:BleTransportLinkRegistry.kt$BleTransportLinkRegistry<T>
+ TooManyFunctions:Curve25519FieldArithmetic.kt$Curve25519FieldArithmetic
+ TooManyFunctions:Curve25519PointArithmetic.kt$Curve25519PointArithmetic
TooManyFunctions:Ed25519Fallback.kt$Ed25519Fallback
TooManyFunctions:GattNotifyClient.kt$GattNotifyClient
TooManyFunctions:GattNotifySessionAdapter.kt$BluetoothGattNotifySession : GattNotifySession
TooManyFunctions:GattNotifySessionAdapter.kt$GattNotifySession
TooManyFunctions:JcaCryptoProvider.kt$JcaCryptoProvider : CryptoProvider
- TooManyFunctions:L2capFrameBuffer.kt$L2capFrameBuffer
TopLevelPropertyNaming:ScanResultSupport.kt$private const val L2capSupportedSdkInt: Int = 34
UnnecessaryAbstractClass:GattNotifyCallbackSupport.kt$BaseGattNotifyCallback$BaseGattNotifyCallback
UnreachableCode:BleTransportAdapterLifecycleSupport.kt$advertiser = try { adapter.bluetoothLeAdvertiser } catch (exception: SecurityException) { throw androidPermissionDenied(exception) }
@@ -232,14 +233,14 @@
UnreachableCode:BleTransportAdapterLifecycleSupport.kt$val gattNotifyServer = BluetoothGattNotifyServer( context = context, peerBindings = peerBindings, onUnknownPeerFrame = ::registerProvisionalGattPeer, onClaimedPeerIdentity = ::registerClaimedGattPeer, onFrameReceived = ::enqueueInboundFrame, log = ::log, )
UnreachableCode:BleTransportAdapterLifecycleSupport.kt$val l2capServerSupported = supportsL2capServerSockets()
UnreachableCode:BleTransportAdapterLifecycleSupport.kt$val serverSocket = if (l2capServerSupported) { runCatching { L2capSocketFactory.listenInsecure(adapter) } .onFailure { error -> log("L2CAP server socket unavailable: ${error.message.orEmpty()}") } .getOrNull() } else { log("L2CAP server socket unavailable: runtime capability probe returned false") null }
- UnsafeCallOnNullableType:Ed25519Fallback.kt$Ed25519Fallback$row[digit - 1]!!
- UnsafeCallOnNullableType:Ed25519Fallback.kt$Ed25519Fallback$table[digit - 1]!!
- UnsafeCallOnNullableType:Ed25519Fallback.kt$Ed25519Fallback$table[digit]!!
+ UnsafeCallOnNullableType:Curve25519PointArithmetic.kt$Curve25519PointArithmetic$row[digit - 1]!!
+ UnsafeCallOnNullableType:Curve25519PointArithmetic.kt$Curve25519PointArithmetic$table[digit - 1]!!
+ UnsafeCallOnNullableType:Curve25519PointArithmetic.kt$Curve25519PointArithmetic$table[digit]!!
UnusedParameter:L2capSendSupport.kt$context: L2capSendContext
- VariableNaming:Ed25519Fallback.kt$Ed25519Fallback$val z2_100_0 = fieldElement()
- VariableNaming:Ed25519Fallback.kt$Ed25519Fallback$val z2_10_0 = fieldElement()
- VariableNaming:Ed25519Fallback.kt$Ed25519Fallback$val z2_20_0 = fieldElement()
- VariableNaming:Ed25519Fallback.kt$Ed25519Fallback$val z2_50_0 = fieldElement()
- VariableNaming:Ed25519Fallback.kt$Ed25519Fallback$val z2_5_0 = fieldElement()
+ VariableNaming:Curve25519FieldArithmetic.kt$Curve25519FieldArithmetic$val z2_100_0 = fieldElement()
+ VariableNaming:Curve25519FieldArithmetic.kt$Curve25519FieldArithmetic$val z2_10_0 = fieldElement()
+ VariableNaming:Curve25519FieldArithmetic.kt$Curve25519FieldArithmetic$val z2_20_0 = fieldElement()
+ VariableNaming:Curve25519FieldArithmetic.kt$Curve25519FieldArithmetic$val z2_50_0 = fieldElement()
+ VariableNaming:Curve25519FieldArithmetic.kt$Curve25519FieldArithmetic$val z2_5_0 = fieldElement()
diff --git a/meshlink/src/androidMain/kotlin/ch/trancee/meshlink/platform/android/crypto/Curve25519FieldArithmetic.kt b/meshlink/src/androidMain/kotlin/ch/trancee/meshlink/platform/android/crypto/Curve25519FieldArithmetic.kt
new file mode 100644
index 00000000..ecab703d
--- /dev/null
+++ b/meshlink/src/androidMain/kotlin/ch/trancee/meshlink/platform/android/crypto/Curve25519FieldArithmetic.kt
@@ -0,0 +1,366 @@
+package ch.trancee.meshlink.platform.android.crypto
+
+/**
+ * RFC 8032/TweetNaCl-style radix-2^16 field arithmetic over GF(2^255-19).
+ *
+ * This is extracted verbatim from [Ed25519Fallback] so the signing API can stay focused on key
+ * handling/signature assembly while field operations remain isolated and independently auditable.
+ *
+ * IMPORTANT: This class intentionally preserves the exact arithmetic, carry strategy, and helper
+ * structure from the original fallback implementation. It is a structural extraction only.
+ */
+internal class Curve25519FieldArithmetic {
+ internal fun add(output: LongArray, left: LongArray, right: LongArray): Unit {
+ for (index in 0 until FIELD_ELEMENT_SIZE) {
+ output[index] = left[index] + right[index]
+ }
+ }
+
+ internal fun subtract(output: LongArray, left: LongArray, right: LongArray): Unit {
+ for (index in 0 until FIELD_ELEMENT_SIZE) {
+ output[index] = left[index] - right[index]
+ }
+ }
+
+ internal fun multiply(
+ output: LongArray,
+ left: LongArray,
+ right: LongArray,
+ temp: LongArray,
+ ): Unit {
+ temp.fill(0)
+ for (leftIndex in 0 until FIELD_ELEMENT_SIZE) {
+ val leftValue = left[leftIndex]
+ for (rightIndex in 0 until FIELD_ELEMENT_SIZE) {
+ temp[leftIndex + rightIndex] += leftValue * right[rightIndex]
+ }
+ }
+ for (index in 0 until FIELD_ELEMENT_SIZE - 1) {
+ temp[index] += 38L * temp[index + FIELD_ELEMENT_SIZE]
+ }
+ for (index in 0 until FIELD_ELEMENT_SIZE) {
+ output[index] = temp[index]
+ }
+ carry(output)
+ carry(output)
+ }
+
+ internal fun square(output: LongArray, input: LongArray, temp: LongArray): Unit {
+ // Schoolbook squaring skips recomputing symmetric cross terms (input[i]*input[j] ==
+ // input[j]*input[i]) and doubles them once instead, roughly halving the number of
+ // limb multiplications compared to calling the general multiply(input, input).
+ temp.fill(0)
+ for (leftIndex in 0 until FIELD_ELEMENT_SIZE) {
+ val leftValue = input[leftIndex]
+ temp[leftIndex * 2] += leftValue * leftValue
+ for (rightIndex in leftIndex + 1 until FIELD_ELEMENT_SIZE) {
+ temp[leftIndex + rightIndex] += 2L * leftValue * input[rightIndex]
+ }
+ }
+ for (index in 0 until FIELD_ELEMENT_SIZE - 1) {
+ temp[index] += 38L * temp[index + FIELD_ELEMENT_SIZE]
+ }
+ for (index in 0 until FIELD_ELEMENT_SIZE) {
+ output[index] = temp[index]
+ }
+ carry(output)
+ carry(output)
+ }
+
+ /**
+ * Computes input^-1 mod p via Fermat's little theorem (input^(p-2)), but instead of the naive
+ * one-bit-at-a-time square-and-multiply (254 squarings + 251 multiplications), this uses the
+ * standard curve25519/ed25519 addition chain for the exponent p-2 = 2^255-21 (as used by
+ * ref10/curve25519-donna): it builds up runs of consecutive 1 bits (2^k-1 patterns) via
+ * repeated squaring plus a single multiply per run, cutting the multiplication count from 251
+ * down to 11 while keeping the same 254 squarings.
+ */
+ internal fun invert(output: LongArray, input: LongArray, temp: LongArray): Unit {
+ val z2 = fieldElement()
+ val z9 = fieldElement()
+ val z11 = fieldElement()
+ val z2_5_0 = fieldElement()
+ val z2_10_0 = fieldElement()
+ val z2_20_0 = fieldElement()
+ val z2_50_0 = fieldElement()
+ val z2_100_0 = fieldElement()
+ val t0 = fieldElement()
+ val t1 = fieldElement()
+
+ square(z2, input, temp) // 2
+ square(t0, z2, temp) // 4
+ square(t0, t0, temp) // 8
+ multiply(z9, t0, input, temp) // 9
+ multiply(z11, z9, z2, temp) // 11
+ square(t0, z11, temp) // 22
+ multiply(z2_5_0, t0, z9, temp) // 2^5 - 2^0 = 31
+
+ square(t0, z2_5_0, temp)
+ for (index in 1 until 5) square(t0, t0, temp) // 2^10 - 2^5
+ multiply(z2_10_0, t0, z2_5_0, temp) // 2^10 - 2^0
+
+ square(t0, z2_10_0, temp)
+ for (index in 1 until 10) square(t0, t0, temp) // 2^20 - 2^10
+ multiply(z2_20_0, t0, z2_10_0, temp) // 2^20 - 2^0
+
+ square(t0, z2_20_0, temp)
+ for (index in 1 until 20) square(t0, t0, temp) // 2^40 - 2^20
+ multiply(t1, t0, z2_20_0, temp) // 2^40 - 2^0
+
+ square(t0, t1, temp)
+ for (index in 1 until 10) square(t0, t0, temp) // 2^50 - 2^10
+ multiply(z2_50_0, t0, z2_10_0, temp) // 2^50 - 2^0
+
+ square(t0, z2_50_0, temp)
+ for (index in 1 until 50) square(t0, t0, temp) // 2^100 - 2^50
+ multiply(z2_100_0, t0, z2_50_0, temp) // 2^100 - 2^0
+
+ square(t0, z2_100_0, temp)
+ for (index in 1 until 100) square(t0, t0, temp) // 2^200 - 2^100
+ multiply(t1, t0, z2_100_0, temp) // 2^200 - 2^0
+
+ square(t0, t1, temp)
+ for (index in 1 until 50) square(t0, t0, temp) // 2^250 - 2^50
+ multiply(t0, t0, z2_50_0, temp) // 2^250 - 2^0
+
+ square(t0, t0, temp) // 2^251 - 2^1
+ square(t0, t0, temp) // 2^252 - 2^2
+ square(t0, t0, temp) // 2^253 - 2^3
+ square(t0, t0, temp) // 2^254 - 2^4
+ square(t0, t0, temp) // 2^255 - 2^5
+ multiply(output, t0, z11, temp) // 2^255 - 21
+ }
+
+ /**
+ * Computes input^((p-5)/8) mod p = input^(2^252-3), used to compute a candidate square root
+ * during point decompression. Uses the same style of addition chain as [invert] (ref10's
+ * fe_pow22523), reducing the multiplication count from 249 down to 9 while keeping the same 250
+ * squarings as the naive one-bit-at-a-time approach.
+ */
+ internal fun power2523(output: LongArray, input: LongArray, temp: LongArray): Unit {
+ val z2 = fieldElement()
+ val z9 = fieldElement()
+ val z11 = fieldElement()
+ val z2_5_0 = fieldElement()
+ val z2_10_0 = fieldElement()
+ val z2_20_0 = fieldElement()
+ val z2_50_0 = fieldElement()
+ val z2_100_0 = fieldElement()
+ val t0 = fieldElement()
+ val t1 = fieldElement()
+
+ square(z2, input, temp) // 2
+ square(t0, z2, temp) // 4
+ square(t0, t0, temp) // 8
+ multiply(z9, t0, input, temp) // 9
+ multiply(z11, z9, z2, temp) // 11
+ square(t0, z11, temp) // 22
+ multiply(z2_5_0, t0, z9, temp) // 2^5 - 2^0 = 31
+
+ square(t0, z2_5_0, temp)
+ for (index in 1 until 5) square(t0, t0, temp)
+ multiply(z2_10_0, t0, z2_5_0, temp) // 2^10 - 2^0
+
+ square(t0, z2_10_0, temp)
+ for (index in 1 until 10) square(t0, t0, temp)
+ multiply(z2_20_0, t0, z2_10_0, temp) // 2^20 - 2^0
+
+ square(t0, z2_20_0, temp)
+ for (index in 1 until 20) square(t0, t0, temp)
+ multiply(t1, t0, z2_20_0, temp) // 2^40 - 2^0
+
+ square(t0, t1, temp)
+ for (index in 1 until 10) square(t0, t0, temp)
+ multiply(z2_50_0, t0, z2_10_0, temp) // 2^50 - 2^0
+
+ square(t0, z2_50_0, temp)
+ for (index in 1 until 50) square(t0, t0, temp)
+ multiply(z2_100_0, t0, z2_50_0, temp) // 2^100 - 2^0
+
+ square(t0, z2_100_0, temp)
+ for (index in 1 until 100) square(t0, t0, temp)
+ multiply(t1, t0, z2_100_0, temp) // 2^200 - 2^0
+
+ square(t0, t1, temp)
+ for (index in 1 until 50) square(t0, t0, temp)
+ multiply(t0, t0, z2_50_0, temp) // 2^250 - 2^0
+
+ square(t0, t0, temp) // 2^251 - 2^1
+ square(t0, t0, temp) // 2^252 - 2^2
+ multiply(output, t0, input, temp) // 2^252 - 3
+ }
+
+ internal fun carry(output: LongArray): Unit {
+ var carry = 1L
+ for (index in 0 until FIELD_ELEMENT_SIZE) {
+ val value = output[index] + carry + 65535L
+ carry = value / 65536L
+ output[index] = value - (carry * 65536L)
+ }
+ output[0] += (carry - 1L) + (37L * (carry - 1L))
+ }
+
+ internal fun select(first: LongArray, second: LongArray, bit: Int): Unit {
+ val mask = -bit.toLong()
+ for (index in 0 until FIELD_ELEMENT_SIZE) {
+ val xor = first[index] xor second[index]
+ val delta = mask and xor
+ first[index] = first[index] xor delta
+ second[index] = second[index] xor delta
+ }
+ }
+
+ internal fun pack25519(output: ByteArray, input: LongArray): Unit {
+ val reduced = input.copyOf()
+ val candidate = fieldElement()
+ repeat(3) { carry(reduced) }
+ repeat(2) {
+ candidate[0] = reduced[0] - 0xffedL
+ for (index in 1 until FIELD_ELEMENT_SIZE - 1) {
+ candidate[index] = reduced[index] - 0xffffL - ((candidate[index - 1] shr 16) and 1L)
+ candidate[index - 1] = candidate[index - 1] and 0xffffL
+ }
+ candidate[15] = reduced[15] - 0x7fffL - ((candidate[14] shr 16) and 1L)
+ val borrow = ((candidate[15] shr 16) and 1L).toInt()
+ candidate[14] = candidate[14] and 0xffffL
+ select(reduced, candidate, 1 - borrow)
+ }
+ for (index in 0 until FIELD_ELEMENT_SIZE) {
+ output[index * 2] = (reduced[index] and 0xffL).toByte()
+ output[(index * 2) + 1] = ((reduced[index] shr 8) and 0xffL).toByte()
+ }
+ }
+
+ internal fun unpack25519(output: LongArray, input: ByteArray): Unit {
+ for (index in 0 until FIELD_ELEMENT_SIZE) {
+ output[index] =
+ input[index * 2].toUnsignedLong() + (input[(index * 2) + 1].toUnsignedLong() shl 8)
+ }
+ output[15] = output[15] and 0x7fffL
+ }
+
+ internal fun zeroField(output: LongArray): Unit {
+ output.fill(0)
+ }
+
+ internal fun copyField(output: LongArray, input: LongArray): Unit {
+ input.copyInto(output)
+ }
+
+ private fun Byte.toUnsignedLong(): Long {
+ return (toInt() and 0xFF).toLong()
+ }
+
+ internal companion object {
+ internal fun fieldElement(): LongArray = LongArray(FIELD_ELEMENT_SIZE)
+
+ internal const val FIELD_ELEMENT_SIZE: Int = 16
+ internal const val MULTIPLICATION_SCRATCH_SIZE: Int = 31
+
+ internal val FIELD_ZERO =
+ longArrayOf(0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L)
+
+ internal val FIELD_ONE =
+ longArrayOf(1L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L)
+
+ internal val D =
+ longArrayOf(
+ 0x78a3L,
+ 0x1359L,
+ 0x4dcaL,
+ 0x75ebL,
+ 0xd8abL,
+ 0x4141L,
+ 0x0a4dL,
+ 0x0070L,
+ 0xe898L,
+ 0x7779L,
+ 0x4079L,
+ 0x8cc7L,
+ 0xfe73L,
+ 0x2b6fL,
+ 0x6ceeL,
+ 0x5203L,
+ )
+
+ internal val D2 =
+ longArrayOf(
+ 0xf159L,
+ 0x26b2L,
+ 0x9b94L,
+ 0xebd6L,
+ 0xb156L,
+ 0x8283L,
+ 0x149aL,
+ 0x00e0L,
+ 0xd130L,
+ 0xeef3L,
+ 0x80f2L,
+ 0x198eL,
+ 0xfce7L,
+ 0x56dfL,
+ 0xd9dcL,
+ 0x2406L,
+ )
+
+ internal val BASE_X =
+ longArrayOf(
+ 0xd51aL,
+ 0x8f25L,
+ 0x2d60L,
+ 0xc956L,
+ 0xa7b2L,
+ 0x9525L,
+ 0xc760L,
+ 0x692cL,
+ 0xdc5cL,
+ 0xfdd6L,
+ 0xe231L,
+ 0xc0a4L,
+ 0x53feL,
+ 0xcd6eL,
+ 0x36d3L,
+ 0x2169L,
+ )
+
+ internal val BASE_Y =
+ longArrayOf(
+ 0x6658L,
+ 0x6666L,
+ 0x6666L,
+ 0x6666L,
+ 0x6666L,
+ 0x6666L,
+ 0x6666L,
+ 0x6666L,
+ 0x6666L,
+ 0x6666L,
+ 0x6666L,
+ 0x6666L,
+ 0x6666L,
+ 0x6666L,
+ 0x6666L,
+ 0x6666L,
+ )
+
+ internal val SQRT_MINUS_ONE =
+ longArrayOf(
+ 0xa0b0L,
+ 0x4a0eL,
+ 0x1b27L,
+ 0xc4eeL,
+ 0xe478L,
+ 0xad2fL,
+ 0x1806L,
+ 0x2f43L,
+ 0xd7a7L,
+ 0x3dfbL,
+ 0x0099L,
+ 0x2b4dL,
+ 0xdf0bL,
+ 0x4fc1L,
+ 0x2480L,
+ 0x2b83L,
+ )
+ }
+}
diff --git a/meshlink/src/androidMain/kotlin/ch/trancee/meshlink/platform/android/crypto/Curve25519PointArithmetic.kt b/meshlink/src/androidMain/kotlin/ch/trancee/meshlink/platform/android/crypto/Curve25519PointArithmetic.kt
new file mode 100644
index 00000000..46649632
--- /dev/null
+++ b/meshlink/src/androidMain/kotlin/ch/trancee/meshlink/platform/android/crypto/Curve25519PointArithmetic.kt
@@ -0,0 +1,345 @@
+package ch.trancee.meshlink.platform.android.crypto
+
+/**
+ * Edwards-curve point arithmetic used by [Ed25519Fallback].
+ *
+ * This preserves the exact formulas and constant-time/variable-time distinctions from the original
+ * fallback implementation; the only change is extraction into a dedicated class to isolate this
+ * concern from signing/key-management logic.
+ */
+internal class Curve25519PointArithmetic(private val field: Curve25519FieldArithmetic) {
+ /**
+ * Precomputed radix-16 comb table for the fixed base point: `baseCombTable[block][digit]` =
+ * `digit * 16^block * B`. Built once (lazily, on first use) so every subsequent fixed-base
+ * scalar multiplication (key generation, signing, and half of verification) needs only 64 point
+ * additions and zero point doublings, instead of 256 doublings + 256 additions for a generic
+ * double-and-add ladder. This is the single biggest cost driver in the fallback
+ * signer/verifier, so trading ~64 * 15 precomputed points (~0.5 MB, built once per process) for
+ * that speedup is a deliberate priority-over-footprint tradeoff.
+ */
+ private val baseCombTable: Array> by lazy { buildBaseCombTable() }
+
+ /**
+ * Fixed-base scalar multiplication using the precomputed radix-16 comb table. Runs in constant
+ * time with respect to `scalar` (via masked table selection) because this is used for both
+ * secret scalars (nonce, private key during key generation/signing) and public ones
+ * (verification); the selection cost is negligible compared to the field multiplications it
+ * replaces, so there is no reason to special-case the public callers.
+ */
+ internal fun scalarBase(output: Point, scalar: ByteArray): Unit {
+ require(scalar.size >= SCALAR_SIZE_BYTES) { "Ed25519 scalar must be at least 32 bytes" }
+ setIdentity(output)
+ val scratch = PointScratch()
+ val selected = Point()
+ for (block in 0 until COMB_BLOCK_COUNT) {
+ selectPoint(selected, baseCombTable[block], nibbleAt(scalar, block))
+ add(output, selected, scratch)
+ }
+ }
+
+ /**
+ * Variable-base windowed scalar multiplication (radix-16, MSB-first). Both `point` and `scalar`
+ * are public values at every call site (signature verification only), so this intentionally
+ * branches on scalar digits instead of using constant-time selection, trading side-channel
+ * resistance we don't need here for fewer point operations.
+ */
+ internal fun windowedScalarMultiplyPublic(
+ output: Point,
+ point: Point,
+ scalar: ByteArray,
+ ): Unit {
+ require(scalar.size >= SCALAR_SIZE_BYTES) { "Ed25519 scalar must be at least 32 bytes" }
+
+ val addScratch = PointScratch()
+ val table = arrayOfNulls(COMB_DIGIT_COUNT)
+ table[1] = point.copy()
+ for (digit in 2 until COMB_DIGIT_COUNT) {
+ val next = table[digit - 1]!!.copy()
+ add(next, point, addScratch)
+ table[digit] = next
+ }
+
+ setIdentity(output)
+ val dblScratch = PointScratch()
+ for (block in COMB_BLOCK_COUNT - 1 downTo 0) {
+ repeat(WINDOW_BITS) { double(output, dblScratch) }
+ val digit = nibbleAt(scalar, block)
+ if (digit != 0) {
+ add(output, table[digit]!!, addScratch)
+ }
+ }
+ }
+
+ internal fun add(point: Point, other: Point): Unit {
+ add(point, other, PointScratch())
+ }
+
+ internal fun pack(output: ByteArray, point: Point): Unit {
+ val tx = fieldElement()
+ val ty = fieldElement()
+ val zi = fieldElement()
+ val temp = LongArray(Curve25519FieldArithmetic.MULTIPLICATION_SCRATCH_SIZE)
+ field.invert(zi, point.z, temp)
+ field.multiply(tx, point.x, zi, temp)
+ field.multiply(ty, point.y, zi, temp)
+ field.pack25519(output, ty)
+ output[31] = (output[31].toInt() xor (fieldParity(tx) shl 7)).toByte()
+ }
+
+ internal fun unpackNegative(output: Point, publicKey: ByteArray): Int {
+ val t = fieldElement()
+ val check = fieldElement()
+ val numerator = fieldElement()
+ val denominator = fieldElement()
+ val denominator2 = fieldElement()
+ val denominator4 = fieldElement()
+ val denominator6 = fieldElement()
+ val temp = LongArray(Curve25519FieldArithmetic.MULTIPLICATION_SCRATCH_SIZE)
+
+ field.copyField(output.z, Curve25519FieldArithmetic.FIELD_ONE)
+ field.unpack25519(output.y, publicKey)
+ field.square(numerator, output.y, temp)
+ field.multiply(denominator, numerator, Curve25519FieldArithmetic.D, temp)
+ field.subtract(numerator, numerator, output.z)
+ field.add(denominator, output.z, denominator)
+
+ field.square(denominator2, denominator, temp)
+ field.square(denominator4, denominator2, temp)
+ field.multiply(denominator6, denominator4, denominator2, temp)
+ field.multiply(t, denominator6, numerator, temp)
+ field.multiply(t, t, denominator, temp)
+
+ field.power2523(t, t, temp)
+ field.multiply(t, t, numerator, temp)
+ field.multiply(t, t, denominator, temp)
+ field.multiply(t, t, denominator, temp)
+ field.multiply(output.x, t, denominator, temp)
+
+ field.square(check, output.x, temp)
+ field.multiply(check, check, denominator, temp)
+ if (fieldNotEqual(check, numerator)) {
+ field.multiply(output.x, output.x, Curve25519FieldArithmetic.SQRT_MINUS_ONE, temp)
+ }
+
+ field.square(check, output.x, temp)
+ field.multiply(check, check, denominator, temp)
+ if (fieldNotEqual(check, numerator)) {
+ return -1
+ }
+
+ if (fieldParity(output.x) == ((publicKey[31].toInt() and 0xFF) ushr 7)) {
+ field.subtract(output.x, Curve25519FieldArithmetic.FIELD_ZERO, output.x)
+ }
+ field.multiply(output.t, output.x, output.y, temp)
+ return 0
+ }
+
+ /** Extracts the 4-bit digit covering bits `[4 * block, 4 * block + 3]` of `scalar`. */
+ private fun nibbleAt(scalar: ByteArray, block: Int): Int {
+ val byteValue = scalar[block ushr 1].toInt()
+ return if (block and 1 == 0) byteValue and 0x0F else (byteValue ushr 4) and 0x0F
+ }
+
+ /**
+ * Builds the fixed-base comb table: `table[block][digit]` = `digit * 16^block * B`, for `block`
+ * in `0 until 64` and `digit` in `1 until 16` (digit 0 is never stored; selection treats it as
+ * the identity). This runs once per process (see `baseCombTable`).
+ */
+ private fun buildBaseCombTable(): Array> {
+ val base =
+ Point().also { point ->
+ field.copyField(point.x, Curve25519FieldArithmetic.BASE_X)
+ field.copyField(point.y, Curve25519FieldArithmetic.BASE_Y)
+ field.copyField(point.z, Curve25519FieldArithmetic.FIELD_ONE)
+ field.multiply(
+ point.t,
+ Curve25519FieldArithmetic.BASE_X,
+ Curve25519FieldArithmetic.BASE_Y,
+ LongArray(Curve25519FieldArithmetic.MULTIPLICATION_SCRATCH_SIZE),
+ )
+ }
+
+ var blockBase = base
+ return Array(COMB_BLOCK_COUNT) { block ->
+ val addScratch = PointScratch()
+ val row = arrayOfNulls(COMB_DIGIT_COUNT)
+ row[1] = blockBase
+ for (digit in 2 until COMB_DIGIT_COUNT) {
+ val next = row[digit - 1]!!.copy()
+ add(next, blockBase, addScratch)
+ row[digit] = next
+ }
+
+ if (block != COMB_BLOCK_COUNT - 1) {
+ val nextBlockBase = blockBase.copy()
+ val dblScratch = PointScratch()
+ repeat(WINDOW_BITS) { double(nextBlockBase, dblScratch) }
+ blockBase = nextBlockBase
+ }
+
+ @Suppress("UNCHECKED_CAST") (row as Array)
+ }
+ }
+
+ /**
+ * Constant-time selection of `candidates[digit]` into `output`, where `digit` is in `0 until
+ * 16` and `candidates[0]` is implicitly the identity point (not stored). Scans every candidate
+ * and masks in the match so execution time and memory access pattern do not depend on `digit`.
+ */
+ private fun selectPoint(output: Point, candidates: Array, digit: Int): Unit {
+ setIdentity(output)
+ for (index in 1 until COMB_DIGIT_COUNT) {
+ val mask = maskEquals(digit, index)
+ conditionalCopy(output.x, candidates[index].x, mask)
+ conditionalCopy(output.y, candidates[index].y, mask)
+ conditionalCopy(output.z, candidates[index].z, mask)
+ conditionalCopy(output.t, candidates[index].t, mask)
+ }
+ }
+
+ /** Returns an all-ones mask when `value == target`, otherwise an all-zeros mask. */
+ private fun maskEquals(value: Int, target: Int): Long {
+ val diff = value xor target
+ // `diff` is in [0, 15], so `diff - 1` is -1 (all bits set) exactly when diff == 0, and a
+ // small non-negative number otherwise; sign-extending shift turns that into 0.
+ return ((diff - 1) shr 31).toLong()
+ }
+
+ private fun conditionalCopy(destination: LongArray, source: LongArray, mask: Long): Unit {
+ for (index in 0 until Curve25519FieldArithmetic.FIELD_ELEMENT_SIZE) {
+ destination[index] = (destination[index] and mask.inv()) or (source[index] and mask)
+ }
+ }
+
+ private fun setIdentity(point: Point): Unit {
+ field.zeroField(point.x)
+ field.copyField(point.y, Curve25519FieldArithmetic.FIELD_ONE)
+ field.copyField(point.z, Curve25519FieldArithmetic.FIELD_ONE)
+ field.zeroField(point.t)
+ }
+
+ private fun add(point: Point, other: Point, scratch: PointScratch): Unit {
+ field.subtract(scratch.a, point.y, point.x)
+ field.subtract(scratch.t, other.y, other.x)
+ field.multiply(scratch.a, scratch.a, scratch.t, scratch.temp)
+ field.add(scratch.b, point.x, point.y)
+ field.add(scratch.t, other.x, other.y)
+ field.multiply(scratch.b, scratch.b, scratch.t, scratch.temp)
+ field.multiply(scratch.c, point.t, other.t, scratch.temp)
+ field.multiply(scratch.c, scratch.c, Curve25519FieldArithmetic.D2, scratch.temp)
+ field.multiply(scratch.d, point.z, other.z, scratch.temp)
+ field.add(scratch.d, scratch.d, scratch.d)
+ field.subtract(scratch.e, scratch.b, scratch.a)
+ field.subtract(scratch.f, scratch.d, scratch.c)
+ field.add(scratch.g, scratch.d, scratch.c)
+ field.add(scratch.h, scratch.b, scratch.a)
+ field.multiply(point.x, scratch.e, scratch.f, scratch.temp)
+ field.multiply(point.y, scratch.h, scratch.g, scratch.temp)
+ field.multiply(point.z, scratch.g, scratch.f, scratch.temp)
+ field.multiply(point.t, scratch.e, scratch.h, scratch.temp)
+ }
+
+ /**
+ * Dedicated point doubling (dbl-2008-hwcd) for the twisted Edwards curve with a = -1. This is
+ * mathematically equivalent to `add(point, point, scratch)` but costs 4 squarings + 4
+ * multiplications instead of the unified addition formula's 9 multiplications, which matters
+ * because every scalar-multiplication bit performs one doubling.
+ */
+ private fun double(point: Point, scratch: PointScratch): Unit {
+ field.square(scratch.a, point.x, scratch.temp)
+ field.square(scratch.b, point.y, scratch.temp)
+ field.square(scratch.c, point.z, scratch.temp)
+ field.add(scratch.c, scratch.c, scratch.c)
+ field.add(scratch.h, point.x, point.y)
+ field.square(scratch.e, scratch.h, scratch.temp)
+ field.subtract(scratch.e, scratch.e, scratch.a)
+ field.subtract(scratch.e, scratch.e, scratch.b)
+ // d = a * A = -A since a = -1
+ field.subtract(scratch.d, Curve25519FieldArithmetic.FIELD_ZERO, scratch.a)
+ field.add(scratch.g, scratch.d, scratch.b)
+ field.subtract(scratch.f, scratch.g, scratch.c)
+ field.subtract(scratch.h, scratch.d, scratch.b)
+ field.multiply(point.x, scratch.e, scratch.f, scratch.temp)
+ field.multiply(point.y, scratch.g, scratch.h, scratch.temp)
+ field.multiply(point.t, scratch.e, scratch.h, scratch.temp)
+ field.multiply(point.z, scratch.f, scratch.g, scratch.temp)
+ }
+
+ private fun fieldParity(input: LongArray): Int {
+ val packed = ByteArray(PUBLIC_KEY_SIZE_BYTES)
+ field.pack25519(packed, input)
+ return packed[0].toInt() and 1
+ }
+
+ private fun fieldNotEqual(first: LongArray, second: LongArray): Boolean {
+ val packedFirst = ByteArray(PUBLIC_KEY_SIZE_BYTES)
+ val packedSecond = ByteArray(PUBLIC_KEY_SIZE_BYTES)
+ field.pack25519(packedFirst, first)
+ field.pack25519(packedSecond, second)
+ return !constantTimeEquals(packedFirst, 0, packedSecond, 0, PUBLIC_KEY_SIZE_BYTES)
+ }
+
+ private fun constantTimeEquals(
+ first: ByteArray,
+ firstOffset: Int,
+ second: ByteArray,
+ secondOffset: Int,
+ length: Int,
+ ): Boolean {
+ var diff = 0
+ for (index in 0 until length) {
+ diff =
+ diff or
+ ((first[firstOffset + index].toInt() and 0xFF) xor
+ (second[secondOffset + index].toInt() and 0xFF))
+ }
+ return diff == 0
+ }
+
+ private fun fieldElement(): LongArray {
+ return Curve25519FieldArithmetic.fieldElement()
+ }
+
+ private companion object {
+ private const val PUBLIC_KEY_SIZE_BYTES: Int = 32
+ private const val SCALAR_SIZE_BYTES: Int = 32
+
+ /** Window width (bits) for the radix-16 comb/windowed scalar multiplications. */
+ private const val WINDOW_BITS: Int = 4
+
+ /** Number of 4-bit windows covering a 256-bit scalar (32 bytes * 2 nibbles/byte). */
+ private const val COMB_BLOCK_COUNT: Int = 64
+
+ /** Number of representable digit values per window (0 until 16, digit 0 is implicit). */
+ private const val COMB_DIGIT_COUNT: Int = 16
+ }
+}
+
+internal class Point {
+ val x: LongArray = Curve25519FieldArithmetic.fieldElement()
+ val y: LongArray = Curve25519FieldArithmetic.fieldElement()
+ val z: LongArray = Curve25519FieldArithmetic.fieldElement()
+ val t: LongArray = Curve25519FieldArithmetic.fieldElement()
+
+ fun copy(): Point {
+ return Point().also { point ->
+ x.copyInto(point.x)
+ y.copyInto(point.y)
+ z.copyInto(point.z)
+ t.copyInto(point.t)
+ }
+ }
+}
+
+internal class PointScratch {
+ val a: LongArray = Curve25519FieldArithmetic.fieldElement()
+ val b: LongArray = Curve25519FieldArithmetic.fieldElement()
+ val c: LongArray = Curve25519FieldArithmetic.fieldElement()
+ val d: LongArray = Curve25519FieldArithmetic.fieldElement()
+ val e: LongArray = Curve25519FieldArithmetic.fieldElement()
+ val f: LongArray = Curve25519FieldArithmetic.fieldElement()
+ val g: LongArray = Curve25519FieldArithmetic.fieldElement()
+ val h: LongArray = Curve25519FieldArithmetic.fieldElement()
+ val t: LongArray = Curve25519FieldArithmetic.fieldElement()
+ val temp: LongArray = LongArray(Curve25519FieldArithmetic.MULTIPLICATION_SCRATCH_SIZE)
+}
diff --git a/meshlink/src/androidMain/kotlin/ch/trancee/meshlink/platform/android/crypto/Ed25519Fallback.kt b/meshlink/src/androidMain/kotlin/ch/trancee/meshlink/platform/android/crypto/Ed25519Fallback.kt
index 8dae5948..d823ff75 100644
--- a/meshlink/src/androidMain/kotlin/ch/trancee/meshlink/platform/android/crypto/Ed25519Fallback.kt
+++ b/meshlink/src/androidMain/kotlin/ch/trancee/meshlink/platform/android/crypto/Ed25519Fallback.kt
@@ -24,16 +24,8 @@ internal class Ed25519Fallback(private val randomBytesProvider: (Int) -> ByteArr
}
}
- /**
- * Precomputed radix-16 comb table for the fixed base point: `baseCombTable[block][digit]` =
- * `digit * 16^block * B`. Built once (lazily, on first use) so every subsequent fixed-base
- * scalar multiplication (key generation, signing, and half of verification) needs only 64 point
- * additions and zero point doublings, instead of 256 doublings + 256 additions for a generic
- * double-and-add ladder. This is the single biggest cost driver in the fallback
- * signer/verifier, so trading ~64 * 15 precomputed points (~0.5 MB, built once per process) for
- * that speedup is a deliberate priority-over-footprint tradeoff.
- */
- private val baseCombTable: Array> by lazy { buildBaseCombTable() }
+ private val fieldArithmetic = Curve25519FieldArithmetic()
+ private val pointArithmetic = Curve25519PointArithmetic(fieldArithmetic)
internal fun generateKeyPair(): Ed25519KeyPair {
val privateKey =
@@ -61,9 +53,9 @@ internal class Ed25519Fallback(private val randomBytesProvider: (Int) -> ByteArr
val expanded = expandedPrivateKey(privateKey)
val reducedNonce = reduceScalar(sha512(expanded.prefix, message))
val noncePoint = Point()
- scalarBase(noncePoint, reducedNonce)
+ pointArithmetic.scalarBase(noncePoint, reducedNonce)
val encodedR = ByteArray(PUBLIC_KEY_SIZE_BYTES)
- pack(encodedR, noncePoint)
+ pointArithmetic.pack(encodedR, noncePoint)
val reducedChallenge = reduceScalar(sha512(encodedR, expanded.publicKey, message))
val s = ByteArray(SCALAR_SIZE_BYTES)
@@ -95,7 +87,7 @@ internal class Ed25519Fallback(private val randomBytesProvider: (Int) -> ByteArr
}
val publicPoint = Point()
- if (unpackNegative(publicPoint, publicKey) != 0) {
+ if (pointArithmetic.unpackNegative(publicPoint, publicKey) != 0) {
return false
}
@@ -107,14 +99,14 @@ internal class Ed25519Fallback(private val randomBytesProvider: (Int) -> ByteArr
// multiplication does not need to be constant-time; a plain windowed method is used
// instead of the constant-time comb selection that fixed-base multiplication requires.
val leftSide = Point()
- windowedScalarMultiplyPublic(leftSide, publicPoint, reducedChallenge)
+ pointArithmetic.windowedScalarMultiplyPublic(leftSide, publicPoint, reducedChallenge)
val rightSide = Point()
- scalarBase(rightSide, scalarComponent)
+ pointArithmetic.scalarBase(rightSide, scalarComponent)
- add(leftSide, rightSide, PointScratch())
+ pointArithmetic.add(leftSide, rightSide)
val encoded = ByteArray(PUBLIC_KEY_SIZE_BYTES)
- pack(encoded, leftSide)
+ pointArithmetic.pack(encoded, leftSide)
return constantTimeEquals(signature, 0, encoded, 0, PUBLIC_KEY_SIZE_BYTES)
}
@@ -135,9 +127,9 @@ internal class Ed25519Fallback(private val randomBytesProvider: (Int) -> ByteArr
val scalar = hash.copyOfRange(0, SCALAR_SIZE_BYTES).also(::clampScalar)
val prefix = hash.copyOfRange(SCALAR_SIZE_BYTES, HASH_SIZE_BYTES)
val publicPoint = Point()
- scalarBase(publicPoint, scalar)
+ pointArithmetic.scalarBase(publicPoint, scalar)
val publicKey = ByteArray(PUBLIC_KEY_SIZE_BYTES)
- pack(publicKey, publicPoint)
+ pointArithmetic.pack(publicKey, publicPoint)
val expanded = ExpandedPrivateKey(scalar = scalar, prefix = prefix, publicKey = publicKey)
synchronized(expandedPrivateKeys) { expandedPrivateKeys[seed] = expanded }
@@ -163,482 +155,6 @@ internal class Ed25519Fallback(private val randomBytesProvider: (Int) -> ByteArr
return digest.digest()
}
- /**
- * Fixed-base scalar multiplication using the precomputed radix-16 comb table. Runs in constant
- * time with respect to `scalar` (via masked table selection) because this is used for both
- * secret scalars (nonce, private key during key generation/signing) and public ones
- * (verification); the selection cost is negligible compared to the field multiplications it
- * replaces, so there is no reason to special-case the public callers.
- */
- private fun scalarBase(output: Point, scalar: ByteArray): Unit {
- require(scalar.size >= SCALAR_SIZE_BYTES) { "Ed25519 scalar must be at least 32 bytes" }
- setIdentity(output)
- val scratch = PointScratch()
- val selected = Point()
- for (block in 0 until COMB_BLOCK_COUNT) {
- selectPoint(selected, baseCombTable[block], nibbleAt(scalar, block))
- add(output, selected, scratch)
- }
- }
-
- /**
- * Variable-base windowed scalar multiplication (radix-16, MSB-first). Both `point` and `scalar`
- * are public values at every call site (signature verification only), so this intentionally
- * branches on scalar digits instead of using constant-time selection, trading side-channel
- * resistance we don't need here for fewer point operations.
- */
- private fun windowedScalarMultiplyPublic(output: Point, point: Point, scalar: ByteArray): Unit {
- require(scalar.size >= SCALAR_SIZE_BYTES) { "Ed25519 scalar must be at least 32 bytes" }
-
- val addScratch = PointScratch()
- val table = arrayOfNulls(COMB_DIGIT_COUNT)
- table[1] = point.copy()
- for (digit in 2 until COMB_DIGIT_COUNT) {
- val next = table[digit - 1]!!.copy()
- add(next, point, addScratch)
- table[digit] = next
- }
-
- setIdentity(output)
- val dblScratch = PointScratch()
- for (block in COMB_BLOCK_COUNT - 1 downTo 0) {
- repeat(WINDOW_BITS) { double(output, dblScratch) }
- val digit = nibbleAt(scalar, block)
- if (digit != 0) {
- add(output, table[digit]!!, addScratch)
- }
- }
- }
-
- /** Extracts the 4-bit digit covering bits `[4 * block, 4 * block + 3]` of `scalar`. */
- private fun nibbleAt(scalar: ByteArray, block: Int): Int {
- val byteValue = scalar[block ushr 1].toInt()
- return if (block and 1 == 0) byteValue and 0x0F else (byteValue ushr 4) and 0x0F
- }
-
- /**
- * Builds the fixed-base comb table: `table[block][digit]` = `digit * 16^block * B`, for `block`
- * in `0 until 64` and `digit` in `1 until 16` (digit 0 is never stored; selection treats it as
- * the identity). This runs once per process (see `baseCombTable`).
- */
- private fun buildBaseCombTable(): Array> {
- val base =
- Point().also { point ->
- copyField(point.x, BASE_X)
- copyField(point.y, BASE_Y)
- copyField(point.z, FIELD_ONE)
- multiply(point.t, BASE_X, BASE_Y, LongArray(MULTIPLICATION_SCRATCH_SIZE))
- }
-
- var blockBase = base
- return Array(COMB_BLOCK_COUNT) { block ->
- val addScratch = PointScratch()
- val row = arrayOfNulls(COMB_DIGIT_COUNT)
- row[1] = blockBase
- for (digit in 2 until COMB_DIGIT_COUNT) {
- val next = row[digit - 1]!!.copy()
- add(next, blockBase, addScratch)
- row[digit] = next
- }
-
- if (block != COMB_BLOCK_COUNT - 1) {
- val nextBlockBase = blockBase.copy()
- val dblScratch = PointScratch()
- repeat(WINDOW_BITS) { double(nextBlockBase, dblScratch) }
- blockBase = nextBlockBase
- }
-
- @Suppress("UNCHECKED_CAST") (row as Array)
- }
- }
-
- /**
- * Constant-time selection of `candidates[digit]` into `output`, where `digit` is in `0 until
- * 16` and `candidates[0]` is implicitly the identity point (not stored). Scans every candidate
- * and masks in the match so execution time and memory access pattern do not depend on `digit`.
- */
- private fun selectPoint(output: Point, candidates: Array, digit: Int): Unit {
- setIdentity(output)
- for (index in 1 until COMB_DIGIT_COUNT) {
- val mask = maskEquals(digit, index)
- conditionalCopy(output.x, candidates[index].x, mask)
- conditionalCopy(output.y, candidates[index].y, mask)
- conditionalCopy(output.z, candidates[index].z, mask)
- conditionalCopy(output.t, candidates[index].t, mask)
- }
- }
-
- /** Returns an all-ones mask when `value == target`, otherwise an all-zeros mask. */
- private fun maskEquals(value: Int, target: Int): Long {
- val diff = value xor target
- // `diff` is in [0, 15], so `diff - 1` is -1 (all bits set) exactly when diff == 0, and a
- // small non-negative number otherwise; sign-extending shift turns that into 0.
- return ((diff - 1) shr 31).toLong()
- }
-
- private fun conditionalCopy(destination: LongArray, source: LongArray, mask: Long): Unit {
- for (index in 0 until FIELD_ELEMENT_SIZE) {
- destination[index] = (destination[index] and mask.inv()) or (source[index] and mask)
- }
- }
-
- private fun setIdentity(point: Point): Unit {
- zeroField(point.x)
- copyField(point.y, FIELD_ONE)
- copyField(point.z, FIELD_ONE)
- zeroField(point.t)
- }
-
- private fun add(point: Point, other: Point, scratch: PointScratch): Unit {
- subtract(scratch.a, point.y, point.x)
- subtract(scratch.t, other.y, other.x)
- multiply(scratch.a, scratch.a, scratch.t, scratch.temp)
- add(scratch.b, point.x, point.y)
- add(scratch.t, other.x, other.y)
- multiply(scratch.b, scratch.b, scratch.t, scratch.temp)
- multiply(scratch.c, point.t, other.t, scratch.temp)
- multiply(scratch.c, scratch.c, D2, scratch.temp)
- multiply(scratch.d, point.z, other.z, scratch.temp)
- add(scratch.d, scratch.d, scratch.d)
- subtract(scratch.e, scratch.b, scratch.a)
- subtract(scratch.f, scratch.d, scratch.c)
- add(scratch.g, scratch.d, scratch.c)
- add(scratch.h, scratch.b, scratch.a)
- multiply(point.x, scratch.e, scratch.f, scratch.temp)
- multiply(point.y, scratch.h, scratch.g, scratch.temp)
- multiply(point.z, scratch.g, scratch.f, scratch.temp)
- multiply(point.t, scratch.e, scratch.h, scratch.temp)
- }
-
- /**
- * Dedicated point doubling (dbl-2008-hwcd) for the twisted Edwards curve with a = -1. This is
- * mathematically equivalent to `add(point, point, scratch)` but costs 4 squarings + 4
- * multiplications instead of the unified addition formula's 9 multiplications, which matters
- * because every scalar-multiplication bit performs one doubling.
- */
- private fun double(point: Point, scratch: PointScratch): Unit {
- square(scratch.a, point.x, scratch.temp)
- square(scratch.b, point.y, scratch.temp)
- square(scratch.c, point.z, scratch.temp)
- add(scratch.c, scratch.c, scratch.c)
- add(scratch.h, point.x, point.y)
- square(scratch.e, scratch.h, scratch.temp)
- subtract(scratch.e, scratch.e, scratch.a)
- subtract(scratch.e, scratch.e, scratch.b)
- // d = a * A = -A since a = -1
- subtract(scratch.d, FIELD_ZERO, scratch.a)
- add(scratch.g, scratch.d, scratch.b)
- subtract(scratch.f, scratch.g, scratch.c)
- subtract(scratch.h, scratch.d, scratch.b)
- multiply(point.x, scratch.e, scratch.f, scratch.temp)
- multiply(point.y, scratch.g, scratch.h, scratch.temp)
- multiply(point.t, scratch.e, scratch.h, scratch.temp)
- multiply(point.z, scratch.f, scratch.g, scratch.temp)
- }
-
- private fun pack(output: ByteArray, point: Point): Unit {
- val tx = fieldElement()
- val ty = fieldElement()
- val zi = fieldElement()
- val temp = LongArray(MULTIPLICATION_SCRATCH_SIZE)
- invert(zi, point.z, temp)
- multiply(tx, point.x, zi, temp)
- multiply(ty, point.y, zi, temp)
- pack25519(output, ty)
- output[31] = (output[31].toInt() xor (fieldParity(tx) shl 7)).toByte()
- }
-
- private fun unpackNegative(output: Point, publicKey: ByteArray): Int {
- val t = fieldElement()
- val check = fieldElement()
- val numerator = fieldElement()
- val denominator = fieldElement()
- val denominator2 = fieldElement()
- val denominator4 = fieldElement()
- val denominator6 = fieldElement()
- val temp = LongArray(MULTIPLICATION_SCRATCH_SIZE)
-
- copyField(output.z, FIELD_ONE)
- unpack25519(output.y, publicKey)
- square(numerator, output.y, temp)
- multiply(denominator, numerator, D, temp)
- subtract(numerator, numerator, output.z)
- add(denominator, output.z, denominator)
-
- square(denominator2, denominator, temp)
- square(denominator4, denominator2, temp)
- multiply(denominator6, denominator4, denominator2, temp)
- multiply(t, denominator6, numerator, temp)
- multiply(t, t, denominator, temp)
-
- power2523(t, t, temp)
- multiply(t, t, numerator, temp)
- multiply(t, t, denominator, temp)
- multiply(t, t, denominator, temp)
- multiply(output.x, t, denominator, temp)
-
- square(check, output.x, temp)
- multiply(check, check, denominator, temp)
- if (fieldNotEqual(check, numerator)) {
- multiply(output.x, output.x, SQRT_MINUS_ONE, temp)
- }
-
- square(check, output.x, temp)
- multiply(check, check, denominator, temp)
- if (fieldNotEqual(check, numerator)) {
- return -1
- }
-
- if (fieldParity(output.x) == ((publicKey[31].toInt() and 0xFF) ushr 7)) {
- subtract(output.x, FIELD_ZERO, output.x)
- }
- multiply(output.t, output.x, output.y, temp)
- return 0
- }
-
- private fun add(output: LongArray, left: LongArray, right: LongArray): Unit {
- for (index in 0 until FIELD_ELEMENT_SIZE) {
- output[index] = left[index] + right[index]
- }
- }
-
- private fun subtract(output: LongArray, left: LongArray, right: LongArray): Unit {
- for (index in 0 until FIELD_ELEMENT_SIZE) {
- output[index] = left[index] - right[index]
- }
- }
-
- private fun multiply(
- output: LongArray,
- left: LongArray,
- right: LongArray,
- temp: LongArray,
- ): Unit {
- temp.fill(0)
- for (leftIndex in 0 until FIELD_ELEMENT_SIZE) {
- val leftValue = left[leftIndex]
- for (rightIndex in 0 until FIELD_ELEMENT_SIZE) {
- temp[leftIndex + rightIndex] += leftValue * right[rightIndex]
- }
- }
- for (index in 0 until FIELD_ELEMENT_SIZE - 1) {
- temp[index] += 38L * temp[index + FIELD_ELEMENT_SIZE]
- }
- for (index in 0 until FIELD_ELEMENT_SIZE) {
- output[index] = temp[index]
- }
- carry(output)
- carry(output)
- }
-
- private fun square(output: LongArray, input: LongArray, temp: LongArray): Unit {
- // Schoolbook squaring skips recomputing symmetric cross terms (input[i]*input[j] ==
- // input[j]*input[i]) and doubles them once instead, roughly halving the number of
- // limb multiplications compared to calling the general multiply(input, input).
- temp.fill(0)
- for (leftIndex in 0 until FIELD_ELEMENT_SIZE) {
- val leftValue = input[leftIndex]
- temp[leftIndex * 2] += leftValue * leftValue
- for (rightIndex in leftIndex + 1 until FIELD_ELEMENT_SIZE) {
- temp[leftIndex + rightIndex] += 2L * leftValue * input[rightIndex]
- }
- }
- for (index in 0 until FIELD_ELEMENT_SIZE - 1) {
- temp[index] += 38L * temp[index + FIELD_ELEMENT_SIZE]
- }
- for (index in 0 until FIELD_ELEMENT_SIZE) {
- output[index] = temp[index]
- }
- carry(output)
- carry(output)
- }
-
- /**
- * Computes input^-1 mod p via Fermat's little theorem (input^(p-2)), but instead of the naive
- * one-bit-at-a-time square-and-multiply (254 squarings + 251 multiplications), this uses the
- * standard curve25519/ed25519 addition chain for the exponent p-2 = 2^255-21 (as used by
- * ref10/curve25519-donna): it builds up runs of consecutive 1 bits (2^k-1 patterns) via
- * repeated squaring plus a single multiply per run, cutting the multiplication count from 251
- * down to 11 while keeping the same 254 squarings.
- */
- private fun invert(output: LongArray, input: LongArray, temp: LongArray): Unit {
- val z2 = fieldElement()
- val z9 = fieldElement()
- val z11 = fieldElement()
- val z2_5_0 = fieldElement()
- val z2_10_0 = fieldElement()
- val z2_20_0 = fieldElement()
- val z2_50_0 = fieldElement()
- val z2_100_0 = fieldElement()
- val t0 = fieldElement()
- val t1 = fieldElement()
-
- square(z2, input, temp) // 2
- square(t0, z2, temp) // 4
- square(t0, t0, temp) // 8
- multiply(z9, t0, input, temp) // 9
- multiply(z11, z9, z2, temp) // 11
- square(t0, z11, temp) // 22
- multiply(z2_5_0, t0, z9, temp) // 2^5 - 2^0 = 31
-
- square(t0, z2_5_0, temp)
- for (index in 1 until 5) square(t0, t0, temp) // 2^10 - 2^5
- multiply(z2_10_0, t0, z2_5_0, temp) // 2^10 - 2^0
-
- square(t0, z2_10_0, temp)
- for (index in 1 until 10) square(t0, t0, temp) // 2^20 - 2^10
- multiply(z2_20_0, t0, z2_10_0, temp) // 2^20 - 2^0
-
- square(t0, z2_20_0, temp)
- for (index in 1 until 20) square(t0, t0, temp) // 2^40 - 2^20
- multiply(t1, t0, z2_20_0, temp) // 2^40 - 2^0
-
- square(t0, t1, temp)
- for (index in 1 until 10) square(t0, t0, temp) // 2^50 - 2^10
- multiply(z2_50_0, t0, z2_10_0, temp) // 2^50 - 2^0
-
- square(t0, z2_50_0, temp)
- for (index in 1 until 50) square(t0, t0, temp) // 2^100 - 2^50
- multiply(z2_100_0, t0, z2_50_0, temp) // 2^100 - 2^0
-
- square(t0, z2_100_0, temp)
- for (index in 1 until 100) square(t0, t0, temp) // 2^200 - 2^100
- multiply(t1, t0, z2_100_0, temp) // 2^200 - 2^0
-
- square(t0, t1, temp)
- for (index in 1 until 50) square(t0, t0, temp) // 2^250 - 2^50
- multiply(t0, t0, z2_50_0, temp) // 2^250 - 2^0
-
- square(t0, t0, temp) // 2^251 - 2^1
- square(t0, t0, temp) // 2^252 - 2^2
- square(t0, t0, temp) // 2^253 - 2^3
- square(t0, t0, temp) // 2^254 - 2^4
- square(t0, t0, temp) // 2^255 - 2^5
- multiply(output, t0, z11, temp) // 2^255 - 21
- }
-
- /**
- * Computes input^((p-5)/8) mod p = input^(2^252-3), used to compute a candidate square root
- * during point decompression. Uses the same style of addition chain as [invert] (ref10's
- * fe_pow22523), reducing the multiplication count from 249 down to 9 while keeping the same 250
- * squarings as the naive one-bit-at-a-time approach.
- */
- private fun power2523(output: LongArray, input: LongArray, temp: LongArray): Unit {
- val z2 = fieldElement()
- val z9 = fieldElement()
- val z11 = fieldElement()
- val z2_5_0 = fieldElement()
- val z2_10_0 = fieldElement()
- val z2_20_0 = fieldElement()
- val z2_50_0 = fieldElement()
- val z2_100_0 = fieldElement()
- val t0 = fieldElement()
- val t1 = fieldElement()
-
- square(z2, input, temp) // 2
- square(t0, z2, temp) // 4
- square(t0, t0, temp) // 8
- multiply(z9, t0, input, temp) // 9
- multiply(z11, z9, z2, temp) // 11
- square(t0, z11, temp) // 22
- multiply(z2_5_0, t0, z9, temp) // 2^5 - 2^0 = 31
-
- square(t0, z2_5_0, temp)
- for (index in 1 until 5) square(t0, t0, temp)
- multiply(z2_10_0, t0, z2_5_0, temp) // 2^10 - 2^0
-
- square(t0, z2_10_0, temp)
- for (index in 1 until 10) square(t0, t0, temp)
- multiply(z2_20_0, t0, z2_10_0, temp) // 2^20 - 2^0
-
- square(t0, z2_20_0, temp)
- for (index in 1 until 20) square(t0, t0, temp)
- multiply(t1, t0, z2_20_0, temp) // 2^40 - 2^0
-
- square(t0, t1, temp)
- for (index in 1 until 10) square(t0, t0, temp)
- multiply(z2_50_0, t0, z2_10_0, temp) // 2^50 - 2^0
-
- square(t0, z2_50_0, temp)
- for (index in 1 until 50) square(t0, t0, temp)
- multiply(z2_100_0, t0, z2_50_0, temp) // 2^100 - 2^0
-
- square(t0, z2_100_0, temp)
- for (index in 1 until 100) square(t0, t0, temp)
- multiply(t1, t0, z2_100_0, temp) // 2^200 - 2^0
-
- square(t0, t1, temp)
- for (index in 1 until 50) square(t0, t0, temp)
- multiply(t0, t0, z2_50_0, temp) // 2^250 - 2^0
-
- square(t0, t0, temp) // 2^251 - 2^1
- square(t0, t0, temp) // 2^252 - 2^2
- multiply(output, t0, input, temp) // 2^252 - 3
- }
-
- private fun carry(output: LongArray): Unit {
- var carry = 1L
- for (index in 0 until FIELD_ELEMENT_SIZE) {
- val value = output[index] + carry + 65535L
- carry = value / 65536L
- output[index] = value - (carry * 65536L)
- }
- output[0] += (carry - 1L) + (37L * (carry - 1L))
- }
-
- private fun select(first: LongArray, second: LongArray, bit: Int): Unit {
- val mask = -bit.toLong()
- for (index in 0 until FIELD_ELEMENT_SIZE) {
- val xor = first[index] xor second[index]
- val delta = mask and xor
- first[index] = first[index] xor delta
- second[index] = second[index] xor delta
- }
- }
-
- private fun pack25519(output: ByteArray, input: LongArray): Unit {
- val reduced = input.copyOf()
- val candidate = fieldElement()
- repeat(3) { carry(reduced) }
- repeat(2) {
- candidate[0] = reduced[0] - 0xffedL
- for (index in 1 until FIELD_ELEMENT_SIZE - 1) {
- candidate[index] = reduced[index] - 0xffffL - ((candidate[index - 1] shr 16) and 1L)
- candidate[index - 1] = candidate[index - 1] and 0xffffL
- }
- candidate[15] = reduced[15] - 0x7fffL - ((candidate[14] shr 16) and 1L)
- val borrow = ((candidate[15] shr 16) and 1L).toInt()
- candidate[14] = candidate[14] and 0xffffL
- select(reduced, candidate, 1 - borrow)
- }
- for (index in 0 until FIELD_ELEMENT_SIZE) {
- output[index * 2] = (reduced[index] and 0xffL).toByte()
- output[(index * 2) + 1] = ((reduced[index] shr 8) and 0xffL).toByte()
- }
- }
-
- private fun unpack25519(output: LongArray, input: ByteArray): Unit {
- for (index in 0 until FIELD_ELEMENT_SIZE) {
- output[index] =
- input[index * 2].toUnsignedLong() + (input[(index * 2) + 1].toUnsignedLong() shl 8)
- }
- output[15] = output[15] and 0x7fffL
- }
-
- private fun fieldParity(input: LongArray): Int {
- val packed = ByteArray(PUBLIC_KEY_SIZE_BYTES)
- pack25519(packed, input)
- return packed[0].toInt() and 1
- }
-
- private fun fieldNotEqual(first: LongArray, second: LongArray): Boolean {
- val packedFirst = ByteArray(PUBLIC_KEY_SIZE_BYTES)
- val packedSecond = ByteArray(PUBLIC_KEY_SIZE_BYTES)
- pack25519(packedFirst, first)
- pack25519(packedSecond, second)
- return !constantTimeEquals(packedFirst, 0, packedSecond, 0, PUBLIC_KEY_SIZE_BYTES)
- }
-
private fun reduce(input: ByteArray): Unit {
val expanded = LongArray(64) { index -> input[index].toUnsignedLong() }
input.fill(0)
@@ -711,14 +227,6 @@ internal class Ed25519Fallback(private val randomBytesProvider: (Int) -> ByteArr
return diff == 0
}
- private fun zeroField(output: LongArray): Unit {
- output.fill(0)
- }
-
- private fun copyField(output: LongArray, input: LongArray): Unit {
- input.copyInto(output)
- }
-
private fun requireSized(value: ByteArray, expectedSize: Int, label: String): ByteArray {
if (value.size != expectedSize) {
throw MeshLinkException.CryptoFailure("$label key must be $expectedSize bytes")
@@ -736,161 +244,13 @@ internal class Ed25519Fallback(private val randomBytesProvider: (Int) -> ByteArr
val publicKey: ByteArray = publicKey.copyOf()
}
- private class Point {
- val x: LongArray = fieldElement()
- val y: LongArray = fieldElement()
- val z: LongArray = fieldElement()
- val t: LongArray = fieldElement()
-
- fun copy(): Point {
- return Point().also { point ->
- x.copyInto(point.x)
- y.copyInto(point.y)
- z.copyInto(point.z)
- t.copyInto(point.t)
- }
- }
- }
-
- private class PointScratch {
- val a: LongArray = fieldElement()
- val b: LongArray = fieldElement()
- val c: LongArray = fieldElement()
- val d: LongArray = fieldElement()
- val e: LongArray = fieldElement()
- val f: LongArray = fieldElement()
- val g: LongArray = fieldElement()
- val h: LongArray = fieldElement()
- val t: LongArray = fieldElement()
- val temp: LongArray = LongArray(MULTIPLICATION_SCRATCH_SIZE)
- }
-
private companion object {
- private fun fieldElement(): LongArray = LongArray(FIELD_ELEMENT_SIZE)
-
- private const val FIELD_ELEMENT_SIZE: Int = 16
private const val HASH_SIZE_BYTES: Int = 64
- private const val MULTIPLICATION_SCRATCH_SIZE: Int = 31
private const val PRIVATE_KEY_SIZE_BYTES: Int = 32
private const val PUBLIC_KEY_SIZE_BYTES: Int = 32
private const val SCALAR_SIZE_BYTES: Int = 32
private const val SIGNATURE_SIZE_BYTES: Int = 64
- /** Window width (bits) for the radix-16 comb/windowed scalar multiplications. */
- private const val WINDOW_BITS: Int = 4
-
- /** Number of 4-bit windows covering a 256-bit scalar (32 bytes * 2 nibbles/byte). */
- private const val COMB_BLOCK_COUNT: Int = 64
-
- /** Number of representable digit values per window (0 until 16, digit 0 is implicit). */
- private const val COMB_DIGIT_COUNT: Int = 16
-
- private val FIELD_ZERO =
- longArrayOf(0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L)
-
- private val FIELD_ONE =
- longArrayOf(1L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L)
-
- private val D =
- longArrayOf(
- 0x78a3L,
- 0x1359L,
- 0x4dcaL,
- 0x75ebL,
- 0xd8abL,
- 0x4141L,
- 0x0a4dL,
- 0x0070L,
- 0xe898L,
- 0x7779L,
- 0x4079L,
- 0x8cc7L,
- 0xfe73L,
- 0x2b6fL,
- 0x6ceeL,
- 0x5203L,
- )
-
- private val D2 =
- longArrayOf(
- 0xf159L,
- 0x26b2L,
- 0x9b94L,
- 0xebd6L,
- 0xb156L,
- 0x8283L,
- 0x149aL,
- 0x00e0L,
- 0xd130L,
- 0xeef3L,
- 0x80f2L,
- 0x198eL,
- 0xfce7L,
- 0x56dfL,
- 0xd9dcL,
- 0x2406L,
- )
-
- private val BASE_X =
- longArrayOf(
- 0xd51aL,
- 0x8f25L,
- 0x2d60L,
- 0xc956L,
- 0xa7b2L,
- 0x9525L,
- 0xc760L,
- 0x692cL,
- 0xdc5cL,
- 0xfdd6L,
- 0xe231L,
- 0xc0a4L,
- 0x53feL,
- 0xcd6eL,
- 0x36d3L,
- 0x2169L,
- )
-
- private val BASE_Y =
- longArrayOf(
- 0x6658L,
- 0x6666L,
- 0x6666L,
- 0x6666L,
- 0x6666L,
- 0x6666L,
- 0x6666L,
- 0x6666L,
- 0x6666L,
- 0x6666L,
- 0x6666L,
- 0x6666L,
- 0x6666L,
- 0x6666L,
- 0x6666L,
- 0x6666L,
- )
-
- private val SQRT_MINUS_ONE =
- longArrayOf(
- 0xa0b0L,
- 0x4a0eL,
- 0x1b27L,
- 0xc4eeL,
- 0xe478L,
- 0xad2fL,
- 0x1806L,
- 0x2f43L,
- 0xd7a7L,
- 0x3dfbL,
- 0x0099L,
- 0x2b4dL,
- 0xdf0bL,
- 0x4fc1L,
- 0x2480L,
- 0x2b83L,
- )
-
private val GROUP_ORDER =
longArrayOf(
0xedL,