refactor: extract KeyCodec.kt from Point.kt for key encoding/decoding
Moves public key parsing, serialization, liftX, and hasEvenY into a dedicated KeyCodec object. These functions operate on field math only (FieldP, U256) and don't use EC point operations or scratch buffers, making them a natural separate concern. Point.kt retains thin delegation methods (liftX, hasEvenY, parsePublicKey, serializeCompressed, serializeUncompressed) so all existing callers (ECPoint.liftX, etc.) continue to work without changes. Point.kt: 803 → 710 lines KeyCodec.kt: 133 lines (new) No functional changes — pure reorganization. https://claude.ai/code/session_01BhU63WUe9AhikZxRdw3Lpg
This commit is contained in:
@@ -0,0 +1,135 @@
|
|||||||
|
/*
|
||||||
|
* 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.secp256k1
|
||||||
|
|
||||||
|
// =====================================================================================
|
||||||
|
// KEY ENCODING, DECODING, AND COORDINATE CONVERSION FOR secp256k1
|
||||||
|
// =====================================================================================
|
||||||
|
//
|
||||||
|
// Converts between public key formats and handles the math for decompressing
|
||||||
|
// compressed keys (recovering y from x via square root on the curve y² = x³ + 7).
|
||||||
|
//
|
||||||
|
// Formats:
|
||||||
|
// - x-only (32 bytes): BIP-340 convention, even y assumed
|
||||||
|
// - Compressed (33 bytes): 02/03 prefix indicates y parity, followed by x
|
||||||
|
// - Uncompressed (65 bytes): 04 prefix, followed by x and y
|
||||||
|
// - Jacobian (X, Y, Z): internal projective representation, needs inversion to convert
|
||||||
|
// =====================================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public key encoding, decoding, and coordinate conversion for secp256k1.
|
||||||
|
*/
|
||||||
|
internal object KeyCodec {
|
||||||
|
/** Curve constant b = 7 in y² = x³ + 7. */
|
||||||
|
private val B = intArrayOf(7, 0, 0, 0, 0, 0, 0, 0)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lift an x-coordinate to a curve point with even y (BIP-340 convention).
|
||||||
|
* Computes y = √(x³ + 7) mod p. Returns false if x is not a valid coordinate.
|
||||||
|
*/
|
||||||
|
fun liftX(
|
||||||
|
outX: IntArray,
|
||||||
|
outY: IntArray,
|
||||||
|
x: IntArray,
|
||||||
|
): Boolean {
|
||||||
|
if (U256.cmp(x, FieldP.P) >= 0) return false
|
||||||
|
val t = IntArray(8)
|
||||||
|
FieldP.sqr(t, x)
|
||||||
|
FieldP.mul(t, t, x)
|
||||||
|
FieldP.add(t, t, B) // t = x³ + 7
|
||||||
|
if (!FieldP.sqrt(outY, t)) return false
|
||||||
|
U256.copyInto(outX, x)
|
||||||
|
if (outY[0] and 1 != 0) FieldP.neg(outY, outY) // Ensure even y
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check if y-coordinate is even (LSB = 0). */
|
||||||
|
fun hasEvenY(y: IntArray): Boolean = y[0] and 1 == 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a serialized public key (33 bytes compressed or 65 bytes uncompressed).
|
||||||
|
* For compressed keys (02/03 prefix): decompresses y from x via square root.
|
||||||
|
* For uncompressed keys (04 prefix): validates the point is on the curve.
|
||||||
|
*/
|
||||||
|
fun parsePublicKey(
|
||||||
|
pubkey: ByteArray,
|
||||||
|
outX: IntArray,
|
||||||
|
outY: IntArray,
|
||||||
|
): Boolean =
|
||||||
|
when {
|
||||||
|
pubkey.size == 33 && (pubkey[0] == 0x02.toByte() || pubkey[0] == 0x03.toByte()) -> {
|
||||||
|
val x = U256.fromBytes(pubkey.copyOfRange(1, 33))
|
||||||
|
if (U256.cmp(x, FieldP.P) >= 0) return false
|
||||||
|
val t = IntArray(8)
|
||||||
|
FieldP.sqr(t, x)
|
||||||
|
FieldP.mul(t, t, x)
|
||||||
|
FieldP.add(t, t, B) // y² = x³ + 7
|
||||||
|
if (!FieldP.sqrt(outY, t)) return false
|
||||||
|
U256.copyInto(outX, x)
|
||||||
|
val isOdd = outY[0] and 1 == 1
|
||||||
|
if (isOdd != (pubkey[0] == 0x03.toByte())) FieldP.neg(outY, outY)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pubkey.size == 65 && pubkey[0] == 0x04.toByte() -> {
|
||||||
|
val x = U256.fromBytes(pubkey.copyOfRange(1, 33))
|
||||||
|
val y = U256.fromBytes(pubkey.copyOfRange(33, 65))
|
||||||
|
val y2 = IntArray(8)
|
||||||
|
val x3p7 = IntArray(8)
|
||||||
|
val t = IntArray(8)
|
||||||
|
FieldP.sqr(y2, y)
|
||||||
|
FieldP.sqr(t, x)
|
||||||
|
FieldP.mul(x3p7, t, x)
|
||||||
|
FieldP.add(x3p7, x3p7, B)
|
||||||
|
if (U256.cmp(y2, x3p7) != 0) return false
|
||||||
|
U256.copyInto(outX, x)
|
||||||
|
U256.copyInto(outY, y)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Serialize as 65-byte uncompressed: 04 || x (32 bytes) || y (32 bytes). */
|
||||||
|
fun serializeUncompressed(
|
||||||
|
x: IntArray,
|
||||||
|
y: IntArray,
|
||||||
|
): ByteArray {
|
||||||
|
val r = ByteArray(65)
|
||||||
|
r[0] = 0x04
|
||||||
|
U256.toBytesInto(x, r, 1)
|
||||||
|
U256.toBytesInto(y, r, 33)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Serialize as 33-byte compressed: 02/03 || x (32 bytes). */
|
||||||
|
fun serializeCompressed(
|
||||||
|
x: IntArray,
|
||||||
|
y: IntArray,
|
||||||
|
): ByteArray {
|
||||||
|
val r = ByteArray(33)
|
||||||
|
r[0] = if (hasEvenY(y)) 0x02 else 0x03
|
||||||
|
U256.toBytesInto(x, r, 1)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -700,104 +700,29 @@ internal object ECPoint {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Key Parsing and Serialization ====================
|
// ==================== Key Encoding (delegates to KeyCodec) ====================
|
||||||
|
|
||||||
/**
|
|
||||||
* Lift an x-coordinate to a curve point with even y (BIP-340 convention).
|
|
||||||
* Computes y = √(x³ + 7) mod p. Returns false if x is not a valid coordinate.
|
|
||||||
*/
|
|
||||||
fun liftX(
|
fun liftX(
|
||||||
outX: IntArray,
|
outX: IntArray,
|
||||||
outY: IntArray,
|
outY: IntArray,
|
||||||
x: IntArray,
|
x: IntArray,
|
||||||
): Boolean {
|
) = KeyCodec.liftX(outX, outY, x)
|
||||||
if (U256.cmp(x, FieldP.P) >= 0) return false
|
|
||||||
val t = IntArray(8)
|
|
||||||
FieldP.sqr(t, x)
|
|
||||||
FieldP.mul(t, t, x)
|
|
||||||
FieldP.add(t, t, B) // t = x³ + 7
|
|
||||||
if (!FieldP.sqrt(outY, t)) return false
|
|
||||||
U256.copyInto(outX, x)
|
|
||||||
if (outY[0] and 1 != 0) FieldP.neg(outY, outY) // Ensure even y
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Check if y-coordinate is even (LSB = 0). */
|
fun hasEvenY(y: IntArray) = KeyCodec.hasEvenY(y)
|
||||||
fun hasEvenY(y: IntArray): Boolean = y[0] and 1 == 0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse a serialized public key (33 bytes compressed or 65 bytes uncompressed).
|
|
||||||
* For compressed keys (02/03 prefix): decompresses y from x via square root.
|
|
||||||
* For uncompressed keys (04 prefix): validates the point is on the curve.
|
|
||||||
*/
|
|
||||||
fun parsePublicKey(
|
fun parsePublicKey(
|
||||||
pubkey: ByteArray,
|
pubkey: ByteArray,
|
||||||
outX: IntArray,
|
outX: IntArray,
|
||||||
outY: IntArray,
|
outY: IntArray,
|
||||||
): Boolean =
|
) = KeyCodec.parsePublicKey(pubkey, outX, outY)
|
||||||
when {
|
|
||||||
pubkey.size == 33 && (pubkey[0] == 0x02.toByte() || pubkey[0] == 0x03.toByte()) -> {
|
|
||||||
val x = U256.fromBytes(pubkey.copyOfRange(1, 33))
|
|
||||||
if (U256.cmp(x, FieldP.P) >= 0) return false
|
|
||||||
val t = IntArray(8)
|
|
||||||
FieldP.sqr(t, x)
|
|
||||||
FieldP.mul(t, t, x)
|
|
||||||
FieldP.add(t, t, B) // y² = x³ + 7
|
|
||||||
if (!FieldP.sqrt(outY, t)) return false
|
|
||||||
U256.copyInto(outX, x)
|
|
||||||
val isOdd = outY[0] and 1 == 1
|
|
||||||
if (isOdd != (pubkey[0] == 0x03.toByte())) FieldP.neg(outY, outY)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
pubkey.size == 65 && pubkey[0] == 0x04.toByte() -> {
|
|
||||||
val x = U256.fromBytes(pubkey.copyOfRange(1, 33))
|
|
||||||
val y = U256.fromBytes(pubkey.copyOfRange(33, 65))
|
|
||||||
val y2 = IntArray(8)
|
|
||||||
val x3p7 = IntArray(8)
|
|
||||||
val t = IntArray(8)
|
|
||||||
FieldP.sqr(y2, y)
|
|
||||||
FieldP.sqr(t, x)
|
|
||||||
FieldP.mul(x3p7, t, x)
|
|
||||||
FieldP.add(x3p7, x3p7, B)
|
|
||||||
if (U256.cmp(y2, x3p7) != 0) return false
|
|
||||||
U256.copyInto(outX, x)
|
|
||||||
U256.copyInto(outY, y)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Serialize as 65-byte uncompressed: 04 || x (32 bytes) || y (32 bytes). */
|
|
||||||
fun serializeUncompressed(
|
fun serializeUncompressed(
|
||||||
x: IntArray,
|
x: IntArray,
|
||||||
y: IntArray,
|
y: IntArray,
|
||||||
): ByteArray {
|
) = KeyCodec.serializeUncompressed(x, y)
|
||||||
val r = ByteArray(65)
|
|
||||||
r[0] = 0x04
|
|
||||||
U256.toBytesInto(x, r, 1)
|
|
||||||
U256.toBytesInto(y, r, 33)
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Serialize as 33-byte compressed: 02/03 || x (32 bytes). */
|
|
||||||
fun serializeCompressed(
|
fun serializeCompressed(
|
||||||
x: IntArray,
|
x: IntArray,
|
||||||
y: IntArray,
|
y: IntArray,
|
||||||
): ByteArray {
|
) = KeyCodec.serializeCompressed(x, y)
|
||||||
val r = ByteArray(33)
|
|
||||||
r[0] = if (hasEvenY(y)) 0x02 else 0x03
|
|
||||||
U256.toBytesInto(x, r, 1)
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Convenience: convert to affine and return as Pair, or null if infinity. */
|
|
||||||
fun toAffinePair(p: MutablePoint): Pair<IntArray, IntArray>? {
|
|
||||||
val x = IntArray(8)
|
|
||||||
val y = IntArray(8)
|
|
||||||
return if (toAffine(p, x, y)) Pair(x, y) else null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user