From ddb62137490ea15d1566b09ecd9c4a29efceafb4 Mon Sep 17 00:00:00 2001 From: PewPewCricket Date: Sat, 26 Sep 2026 11:35:11 -0500 Subject: [PATCH 1/4] curve 25519 stuff --- .../li/cil/oc/server/component/DataCard.scala | 117 ++++++++++++++++-- 1 file changed, 108 insertions(+), 9 deletions(-) diff --git a/src/main/scala/li/cil/oc/server/component/DataCard.scala b/src/main/scala/li/cil/oc/server/component/DataCard.scala index a4fbe526aa..f54174bb40 100644 --- a/src/main/scala/li/cil/oc/server/component/DataCard.scala +++ b/src/main/scala/li/cil/oc/server/component/DataCard.scala @@ -2,6 +2,11 @@ package li.cil.oc.server.component import java.security._ import java.security.interfaces.ECPublicKey +import java.security.interfaces.ECPrivateKey +import java.security.interfaces.EdECPrivateKey +import java.security.interfaces.EdECPublicKey +import java.security.interfaces.XECPrivateKey +import java.security.interfaces.XECPublicKey import java.security.spec.PKCS8EncodedKeySpec import java.security.spec.X509EncodedKeySpec import java.util @@ -240,6 +245,23 @@ object DataCard { result(new ECUserdata(kp.getPublic), new ECUserdata(kp.getPrivate)) } + @Callback(direct = true, limit = 1, doc = """function(keyType:string):userdata, userdata -- Generates an ed25519/x25519 private key.""") + def generate25519Keypair(context: Context, args: Arguments): Array[AnyRef] = { + checkCost(Settings.get.dataCardAsymmetric) + val keyType = args.checkString(0) + val algorithm = keyType match { + case "ed25519" => "Ed25519" + case "x25519" => "X25519" + case _ => throw new IllegalArgumentException("invalid key type, must be ed25519 or x25519") + } + + val kpg = KeyPairGenerator.getInstance(algorithm) + kpg.initialize(255, SecureRandomInstance.get) + val kp = kpg.generateKeyPair() + + result(new ECUserdata(kp.getPublic), new ECUserdata(kp.getPrivate)) + } + @Callback(direct = true, limit = 8, doc = """function(data:string, type:string):userdata -- Restores key from its string representation.""") def deserializeKey(context: Context, args: Arguments): Array[AnyRef] = { val data = simpleCost(context, args) @@ -260,6 +282,48 @@ object DataCard { result(ka.generateSecret) } + @Callback(direct = true, limit = 1, doc = """function(priv:userdata, pub:userdata):string -- Generates a shared key using X25519. x25519(a.priv, b.pub) == x25519(b.priv, a.pub)""") + def x25519(context: Context, args: Arguments): Array[AnyRef] = { + checkCost(Settings.get.dataCardAsymmetric) + val privKey = checkX25519Userdata(args, 0, isPublic = Option(false)).value + val pubKey = checkX25519Userdata(args, 1, isPublic = Option(true)).value + + val ka = KeyAgreement.getInstance("X25519") + ka.init(privKey) + ka.doPhase(pubKey, true) + result(ka.generateSecret) + } + + @Callback(direct = true, limit = 1, doc = """function(data:string, key:userdata[, sig:string]):string or boolean -- Signs or verifies data using Ed25519.""") + def ed25519(context: Context, args: Arguments): Array[AnyRef] = { + val data = asymmetricCost(context, args) + val key = checkEd25519Userdata(args, 1) + val sig = args.optByteArray(2, null) + + val sign = Signature.getInstance("Ed25519") + if (sig != null) { + // Verify mode + key.value match { + case public: PublicKey => + sign.initVerify(public) + sign.update(data) + result(sign.verify(sig)) + case _ => throw new IllegalArgumentException("public key expected") + } + } + else { + // Sign mode + key.value match { + case k: PrivateKey => + sign.initSign(k) + sign.update(data) + result(sign.sign()) + case _ => + throw new IllegalArgumentException("private key expected") + } + } + } + @Callback(direct = true, limit = 1, doc = """function(data:string, key:userdata[, sig:string]):string or boolean -- Signs or verifies data.""") def ecdsa(context: Context, args: Arguments): Array[AnyRef] = { val data = asymmetricCost(context, args) @@ -292,7 +356,7 @@ object DataCard { // ----------------------------------------------------------------------- // - private def checkUserdata(args: Arguments, i: Int, isPublic: Option[Boolean] = None) = + private def checkUserdata(args: Arguments, i: Int, isPublic: Option[Boolean] = None) = { args.checkAny(i) match { case value: ECUserdata => if (isPublic.fold(true)(_ == value.isPublic)) value @@ -303,15 +367,40 @@ object DataCard { case value => throw new IllegalArgumentException( s"bad argument #${i + 1} (userdata expected, got ${value.getClass.getName})") } + } + + private def checkX25519Userdata(args: Arguments, i: Int, isPublic: Option[Boolean] = None) = { + val value = checkUserdata(args, i, isPublic) + value.value match { + case _: XECPublicKey | _: XECPrivateKey => value + case _ => throw new IllegalArgumentException(s"bad argument #${i + 1} (x25519 key expected)") + } + } + + private def checkEd25519Userdata(args: Arguments, i: Int, isPublic: Option[Boolean] = None) = { + val value = checkUserdata(args, i, isPublic) + value.value match { + case _: EdECPublicKey | _: EdECPrivateKey => value + case _ => throw new IllegalArgumentException(s"bad argument #${i + 1} (ed25519 key expected)") + } + } } class ECUserdata(var value: Key) extends prefab.AbstractValue { // Empty constructor for deserialization. def this() = this(null) - def isPublic = value.isInstanceOf[ECPublicKey] + def isPublic = value.isInstanceOf[PublicKey] - def keyType = if (isPublic) ECUserdata.PublicTypeName else ECUserdata.PrivateTypeName + def keyType: String = value match { + case _: ECPublicKey => ECUserdata.PublicTypeName + case _: ECPrivateKey => ECUserdata.PrivateTypeName + case _: EdECPublicKey => ECUserdata.Ed25519PublicTypeName + case _: EdECPrivateKey => ECUserdata.Ed25519PrivateTypeName + case _: XECPublicKey => ECUserdata.X25519PublicTypeName + case _: XECPrivateKey => ECUserdata.X25519PrivateTypeName + case _ => throw new IllegalStateException(s"unsupported key type: ${value.getClass.getName}") + } // ----------------------------------------------------------------------- // @@ -338,15 +427,25 @@ object DataCard { } } + object ECUserdata { final val PrivateTypeName = "ec-private" final val PublicTypeName = "ec-public" - - def deserializeKey(typeName: String, data: Array[Byte]): Key = { - if (typeName == PrivateTypeName) KeyFactory.getInstance("EC").generatePrivate(new PKCS8EncodedKeySpec(data)) - else if (typeName == PublicTypeName) KeyFactory.getInstance("EC").generatePublic(new X509EncodedKeySpec(data)) - else throw new IllegalArgumentException("invalid key type, must be ec-public or ec-private") + final val Ed25519PrivateTypeName = "ed25519-private" + final val Ed25519PublicTypeName = "ed25519-public" + final val X25519PrivateTypeName = "x25519-private" + final val X25519PublicTypeName = "x25519-public" + + def deserializeKey(typeName: String, data: Array[Byte]): Key = typeName match { + case PrivateTypeName => KeyFactory.getInstance("EC").generatePrivate(new PKCS8EncodedKeySpec(data)) + case PublicTypeName => KeyFactory.getInstance("EC").generatePublic(new X509EncodedKeySpec(data)) + case Ed25519PrivateTypeName => KeyFactory.getInstance("Ed25519").generatePrivate(new PKCS8EncodedKeySpec(data)) + case Ed25519PublicTypeName => KeyFactory.getInstance("Ed25519").generatePublic(new X509EncodedKeySpec(data)) + case X25519PrivateTypeName => KeyFactory.getInstance("X25519").generatePrivate(new PKCS8EncodedKeySpec(data)) + case X25519PublicTypeName => KeyFactory.getInstance("X25519").generatePublic(new X509EncodedKeySpec(data)) + case _ => throw new IllegalArgumentException( + "invalid key type, must be one of ec-public, ec-private, ed25519-public, ed25519-private, x25519-public, x25519-private") } } -} \ No newline at end of file +} From 1dcbc60b9abac124120882d023d799240fa5ae26 Mon Sep 17 00:00:00 2001 From: PewPewCricket Date: Sat, 26 Sep 2026 11:57:22 -0500 Subject: [PATCH 2/4] fix me being an idiot --- .../li/cil/oc/server/component/DataCard.scala | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/scala/li/cil/oc/server/component/DataCard.scala b/src/main/scala/li/cil/oc/server/component/DataCard.scala index f54174bb40..da446c2c3d 100644 --- a/src/main/scala/li/cil/oc/server/component/DataCard.scala +++ b/src/main/scala/li/cil/oc/server/component/DataCard.scala @@ -245,7 +245,7 @@ object DataCard { result(new ECUserdata(kp.getPublic), new ECUserdata(kp.getPrivate)) } - @Callback(direct = true, limit = 1, doc = """function(keyType:string):userdata, userdata -- Generates an ed25519/x25519 private key.""") + @Callback(direct = true, limit = 1, doc = """function(keyType:string):userdata, userdata -- Generates an ed25519/x25519 key pair.""") def generate25519Keypair(context: Context, args: Arguments): Array[AnyRef] = { checkCost(Settings.get.dataCardAsymmetric) val keyType = args.checkString(0) @@ -294,13 +294,13 @@ object DataCard { result(ka.generateSecret) } - @Callback(direct = true, limit = 1, doc = """function(data:string, key:userdata[, sig:string]):string or boolean -- Signs or verifies data using Ed25519.""") - def ed25519(context: Context, args: Arguments): Array[AnyRef] = { + @Callback(direct = true, limit = 1, doc = """function(data:string, key:userdata[, sig:string]):string or boolean -- Signs or verifies data.""") + def ecdsa(context: Context, args: Arguments): Array[AnyRef] = { val data = asymmetricCost(context, args) - val key = checkEd25519Userdata(args, 1) + val key = checkUserdata(args, 1) val sig = args.optByteArray(2, null) - val sign = Signature.getInstance("Ed25519") + val sign = Signature.getInstance("SHA256withECDSA") if (sig != null) { // Verify mode key.value match { @@ -324,13 +324,13 @@ object DataCard { } } - @Callback(direct = true, limit = 1, doc = """function(data:string, key:userdata[, sig:string]):string or boolean -- Signs or verifies data.""") - def ecdsa(context: Context, args: Arguments): Array[AnyRef] = { + @Callback(direct = true, limit = 1, doc = """function(data:string, key:userdata[, sig:string]):string or boolean -- Signs or verifies data using Ed25519.""") + def ed25519(context: Context, args: Arguments): Array[AnyRef] = { val data = asymmetricCost(context, args) - val key = checkUserdata(args, 1) + val key = checkEd25519Userdata(args, 1) val sig = args.optByteArray(2, null) - val sign = Signature.getInstance("SHA256withECDSA") + val sign = Signature.getInstance("Ed25519") if (sig != null) { // Verify mode key.value match { From d4bd64b992f5a5adaee72dcd9856b0bfe4b267b6 Mon Sep 17 00:00:00 2001 From: PewPewCricket Date: Sat, 26 Sep 2026 12:00:54 -0500 Subject: [PATCH 3/4] me when i fuck up one letter --- src/main/scala/li/cil/oc/server/component/DataCard.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/li/cil/oc/server/component/DataCard.scala b/src/main/scala/li/cil/oc/server/component/DataCard.scala index da446c2c3d..70df18ce31 100644 --- a/src/main/scala/li/cil/oc/server/component/DataCard.scala +++ b/src/main/scala/li/cil/oc/server/component/DataCard.scala @@ -246,7 +246,7 @@ object DataCard { } @Callback(direct = true, limit = 1, doc = """function(keyType:string):userdata, userdata -- Generates an ed25519/x25519 key pair.""") - def generate25519Keypair(context: Context, args: Arguments): Array[AnyRef] = { + def generate25519KeyPair(context: Context, args: Arguments): Array[AnyRef] = { checkCost(Settings.get.dataCardAsymmetric) val keyType = args.checkString(0) val algorithm = keyType match { From 652e303119eb8671be334182834dc00f63e72240 Mon Sep 17 00:00:00 2001 From: PewPewCricket Date: Sat, 26 Sep 2026 14:19:26 -0500 Subject: [PATCH 4/4] Make API not suck while keeping compat. Fix non-EC keys from being accepted by EC functions. --- .../li/cil/oc/server/component/DataCard.scala | 55 +++++++++++-------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/src/main/scala/li/cil/oc/server/component/DataCard.scala b/src/main/scala/li/cil/oc/server/component/DataCard.scala index 70df18ce31..566d389bb9 100644 --- a/src/main/scala/li/cil/oc/server/component/DataCard.scala +++ b/src/main/scala/li/cil/oc/server/component/DataCard.scala @@ -230,34 +230,33 @@ object DataCard { // ----------------------------------------------------------------------- // - @Callback(direct = true, limit = 1, doc = """function([bitLen:number]):userdata, userdata -- Generates key pair. Returns: public, private keys. Allowed key lengths: 256, 384 bits.""") + @Callback(direct = true, limit = 1, doc = """function([bitLen:number[, keyType:string]]):userdata, userdata -- Generates a key pair. keyType is "ec" (default), "ed25519", or "x25519". bitLen only applies to "ec" keys (256 or 384, default 384). Returns: public, private keys.""") def generateKeyPair(context: Context, args: Arguments): Array[AnyRef] = { checkCost(Settings.get.dataCardAsymmetric) - val bitLen = args.optInteger(0, 384) - if (bitLen != 256 && bitLen != 384) - throw new IllegalArgumentException("invalid key length, must be 256 or 384") + val keyType = args.optString(1, "ec") - val kpg = KeyPairGenerator.getInstance("EC") - kpg.initialize(bitLen, SecureRandomInstance.get) - val kp = kpg.generateKeyPair() + val kp = keyType match { + case "ec" => + val bitLen = args.optInteger(0, 384) + if (bitLen != 256 && bitLen != 384) + throw new IllegalArgumentException("invalid key length, must be 256 or 384") + val kpg = KeyPairGenerator.getInstance("EC") + kpg.initialize(bitLen, SecureRandomInstance.get) + kpg.generateKeyPair() - result(new ECUserdata(kp.getPublic), new ECUserdata(kp.getPrivate)) - } + case "ed25519" => + val kpg = KeyPairGenerator.getInstance("Ed25519") + kpg.initialize(255, SecureRandomInstance.get) + kpg.generateKeyPair() - @Callback(direct = true, limit = 1, doc = """function(keyType:string):userdata, userdata -- Generates an ed25519/x25519 key pair.""") - def generate25519KeyPair(context: Context, args: Arguments): Array[AnyRef] = { - checkCost(Settings.get.dataCardAsymmetric) - val keyType = args.checkString(0) - val algorithm = keyType match { - case "ed25519" => "Ed25519" - case "x25519" => "X25519" - case _ => throw new IllegalArgumentException("invalid key type, must be ed25519 or x25519") - } + case "x25519" => + val kpg = KeyPairGenerator.getInstance("X25519") + kpg.initialize(255, SecureRandomInstance.get) + kpg.generateKeyPair() - val kpg = KeyPairGenerator.getInstance(algorithm) - kpg.initialize(255, SecureRandomInstance.get) - val kp = kpg.generateKeyPair() + case _ => throw new IllegalArgumentException("invalid key type, must be ec, ed25519, or x25519") + } result(new ECUserdata(kp.getPublic), new ECUserdata(kp.getPrivate)) } @@ -273,8 +272,8 @@ object DataCard { @Callback(direct = true, limit = 1, doc = """function(priv:userdata, pub:userdata):string -- Generates a shared key. ecdh(a.priv, b.pub) == ecdh(b.priv, a.pub)""") def ecdh(context: Context, args: Arguments): Array[AnyRef] = { checkCost(Settings.get.dataCardAsymmetric) - val privKey = checkUserdata(args, 0, isPublic = Option(false)).value - val pubKey = checkUserdata(args, 1, isPublic = Option(true)).value + val privKey = checkECUserdata(args, 0, isPublic = Option(false)).value + val pubKey = checkECUserdata(args, 1, isPublic = Option(true)).value val ka = KeyAgreement.getInstance("ECDH") ka.init(privKey) @@ -297,7 +296,7 @@ object DataCard { @Callback(direct = true, limit = 1, doc = """function(data:string, key:userdata[, sig:string]):string or boolean -- Signs or verifies data.""") def ecdsa(context: Context, args: Arguments): Array[AnyRef] = { val data = asymmetricCost(context, args) - val key = checkUserdata(args, 1) + val key = checkECUserdata(args, 1) val sig = args.optByteArray(2, null) val sign = Signature.getInstance("SHA256withECDSA") @@ -369,6 +368,14 @@ object DataCard { } } + private def checkECUserdata(args: Arguments, i: Int, isPublic: Option[Boolean] = None) = { + val value = checkUserdata(args, i, isPublic) + value.value match { + case _: ECPublicKey | _: ECPrivateKey => value + case _ => throw new IllegalArgumentException(s"bad argument #${i + 1} (ec key expected)") + } + } + private def checkX25519Userdata(args: Arguments, i: Int, isPublic: Option[Boolean] = None) = { val value = checkUserdata(args, i, isPublic) value.value match {