Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ private static int PackP1363ComponentToDerInteger(
byte[] derSignature,
int derOffset) {

Assert.IsTrue(p1363Offset > 0);
Assert.IsTrue(p1363Offset >= 0);
Assert.IsTrue(p1363ComponentDerIntLength is > 1 and <= P256P1363ComponentLen + 1);

derSignature[derOffset] = P256DerSignatureComponentPrefixType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ public byte[] DecryptSessionPayload(byte[] payload)
{
if (_encryptionKey == null)
{
const string e = "Cannot encrypt, no session key has been established";
const string e = "Cannot decrypt, no session key has been established";
Debug.LogError(e);
throw new InvalidOperationException(e);
}
Expand Down
8 changes: 8 additions & 0 deletions Tests.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions Tests/EditMode.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions Tests/EditMode/Crypto.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

140 changes: 140 additions & 0 deletions Tests/EditMode/Crypto/EcdsaSignaturesTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
using NUnit.Framework;
using System;
using Org.BouncyCastle.Asn1.Sec;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Security;

// ReSharper disable once CheckNamespace
namespace Solana.Unity.SDK.Tests.EditMode.Crypto
{
/// <summary>
/// Edit mode tests for the EcdsaSignatures helpers.
/// These only exercise the crypto conversion logic, so they don't need Android.
/// </summary>
public class EcdsaSignaturesTests
{

// Helpers
private static ECPublicKeyParameters GenerateP256PublicKey()
{
var gen = new ECKeyPairGenerator();
var curve = SecNamedCurves.GetByName("secp256r1");
var domainParams = new ECDomainParameters(curve.Curve, curve.G, curve.N, curve.H);
gen.Init(new ECKeyGenerationParameters(domainParams, new SecureRandom()));
var keyPair = gen.GenerateKeyPair();
return (ECPublicKeyParameters)keyPair.Public;
}

private static byte[] GenerateValidP1363Signature()
{
// Use a real signature so the round-trip test is working with realistic input.
var session = new MobileWalletAdapterSession();
var helloReq = session.CreateHelloReq();
// HELLO_REQ is [65-byte public key | 64-byte P1363 signature].
var p1363 = new byte[64];
Array.Copy(helloReq, 65, p1363, 0, 64);
return p1363;
}


// DER <-> P1363 round-trip
[Test]
public void ConvertDerToP1363_ThenBackToDer_RoundTrip_PreservesSignature()
{
// Start with a valid P1363 signature.
byte[] originalP1363 = GenerateValidP1363Signature();

// Convert to DER and back again.
byte[] der = EcdsaSignatures.ConvertEcp256SignatureP1363ToDer(originalP1363, 0);
byte[] roundTrippedP1363 = EcdsaSignatures.ConvertEcp256SignatureDeRtoP1363(der, 0);

// We should end up with the exact same 64-byte signature.
Assert.AreEqual(64, roundTrippedP1363.Length,
"P1363 signature must always be exactly 64 bytes");
Assert.AreEqual(originalP1363, roundTrippedP1363,
"Round-tripped P1363 signature must be byte-for-byte identical to the original");
}


// DER input validation
[Test]
public void ConvertDerToP1363_ThrowsArgumentException_WhenBufferTooShort()
{
// Too short to even contain the DER prefix.
var tooShort = new byte[1] { 0x30 };

// Act & Assert
var ex = Assert.Throws<ArgumentException>(() =>
EcdsaSignatures.ConvertEcp256SignatureDeRtoP1363(tooShort, 0));

StringAssert.Contains("too short", ex.Message,
"Exception message should mention buffer is too short");
}

[Test]
public void ConvertDerToP1363_ThrowsArgumentException_WhenTypeByte_IsWrong()
{
// Same shape, but the DER type byte is wrong.
var wrongType = new byte[] { 0x31, 0x44, 0x02, 0x20 };

// Act & Assert
var ex = Assert.Throws<ArgumentException>(() =>
EcdsaSignatures.ConvertEcp256SignatureDeRtoP1363(wrongType, 0));

StringAssert.Contains("invalid type", ex.Message,
"Exception message should mention invalid type");
}


// P-256 public key round-trip
[Test]
public void EncodeDecodeP256PublicKey_RoundTrip_PreservesCoordinates()
{
// Arrange
var originalKey = GenerateP256PublicKey();

// Act
byte[] encoded = EcdsaSignatures.EncodeP256PublicKey(originalKey);
ECPublicKeyParameters decoded = EcdsaSignatures.DecodeP256PublicKey(encoded);

// Uncompressed point format is 0x04 + 32-byte X + 32-byte Y.
Assert.AreEqual(65, encoded.Length,
"Encoded public key must be 65 bytes (uncompressed point format)");
Assert.AreEqual((byte)0x04, encoded[0],
"First byte must be 0x04 for uncompressed EC point");

var originalX = originalKey.Q.AffineXCoord.ToBigInteger();
var originalY = originalKey.Q.AffineYCoord.ToBigInteger();
var decodedX = decoded.Q.AffineXCoord.ToBigInteger();
var decodedY = decoded.Q.AffineYCoord.ToBigInteger();

Assert.AreEqual(originalX, decodedX, "X coordinate must survive encode/decode");
Assert.AreEqual(originalY, decodedY, "Y coordinate must survive encode/decode");
}

[Test]
public void DecodeP256PublicKey_ThrowsArgumentException_WhenInputTooShort()
{
// Definitely shorter than a valid uncompressed key.
var tooShort = new byte[10];
tooShort[0] = 0x04;

// Act & Assert
Assert.Throws<ArgumentException>(() =>
EcdsaSignatures.DecodeP256PublicKey(tooShort));
}

[Test]
public void DecodeP256PublicKey_ThrowsArgumentException_WhenPrefixByte_IsWrong()
{
// Right length, wrong point prefix.
var wrongPrefix = new byte[65];
wrongPrefix[0] = 0x02;

// Act & Assert
Assert.Throws<ArgumentException>(() =>
EcdsaSignatures.DecodeP256PublicKey(wrongPrefix));
}
}
}
2 changes: 2 additions & 0 deletions Tests/EditMode/Crypto/EcdsaSignaturesTests.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

136 changes: 136 additions & 0 deletions Tests/EditMode/Crypto/MobileWalletAdapterSessionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
using NUnit.Framework;
using System;
using System.Text.RegularExpressions;
using UnityEngine;
using UnityEngine.TestTools;

// ReSharper disable once CheckNamespace
namespace Solana.Unity.SDK.Tests.EditMode.Crypto
{
/// <summary>
/// Edit mode tests for MobileWalletAdapterSession.
/// These cover the session's pure crypto and encoding behavior without Android.
/// </summary>
public class MobileWalletAdapterSessionTests
{

// AssociationToken format
[Test]
public void AssociationToken_IsValidBase64Url_NoStandardBase64Characters()
{
// Arrange
var session = new MobileWalletAdapterSession();

// Act
string token = session.AssociationToken;

// Base64Url output should avoid the characters that break URLs.
Assert.IsNotNull(token, "AssociationToken must not be null");
Assert.IsNotEmpty(token, "AssociationToken must not be empty");
Assert.IsFalse(token.Contains('+'),
"AssociationToken must not contain '+' (standard Base64 char, breaks URI)");
Assert.IsFalse(token.Contains('/'),
"AssociationToken must not contain '/' (standard Base64 char, breaks URI)");
Assert.IsFalse(token.Contains('='),
"AssociationToken must not contain '=' padding (breaks URI)");
}

[Test]
public void AssociationToken_OnlyContains_ValidBase64UrlCharacters()
{
// Arrange
var session = new MobileWalletAdapterSession();

// Act
string token = session.AssociationToken;

// Only URL-safe Base64 characters should be present.
var validBase64Url = new Regex(@"^[A-Za-z0-9\-_]+$");
Assert.IsTrue(validBase64Url.IsMatch(token),
$"AssociationToken '{token}' contains characters outside Base64Url alphabet");
}

[Test]
public void AssociationToken_IsDerivedFrom_PublicKeyBytes()
{
// Arrange
var session = new MobileWalletAdapterSession();

// Recreate the token from the raw public key bytes.
byte[] pubKeyBytes = session.PublicKeyBytes;
string expected = Convert.ToBase64String(pubKeyBytes)
.Split('=')[0]
.Replace('+', '-')
.Replace('/', '_');

// Assert
Assert.AreEqual(expected, session.AssociationToken,
"AssociationToken must be the Base64Url encoding of PublicKeyBytes");
}


// Error paths before ECDH is set up
[Test]
public void EncryptSessionPayload_ThrowsInvalidOperationException_WhenNoSessionKeyEstablished()
{
// Fresh session; no shared key has been negotiated yet.
var session = new MobileWalletAdapterSession();
var payload = new byte[] { 0x01, 0x02, 0x03 };

// The production code logs before it throws, so the test needs to allow that.
LogAssert.Expect(LogType.Error, "Cannot encrypt, no session key has been established");

// Act & Assert
var ex = Assert.Throws<InvalidOperationException>(() =>
session.EncryptSessionPayload(payload));

StringAssert.Contains("no session key has been established", ex.Message,
"Exception message must mention that no session key has been established");
}

[Test]
public void DecryptSessionPayload_ThrowsInvalidOperationException_WhenNoSessionKeyEstablished()
{
// Fresh session; no shared key has been negotiated yet.
var session = new MobileWalletAdapterSession();
var payload = new byte[64]; // arbitrary non-empty payload

// The production code logs before it throws, so the test needs to allow that.
LogAssert.Expect(LogType.Error, "Cannot decrypt, no session key has been established");

// Act & Assert
Assert.Throws<InvalidOperationException>(() =>
session.DecryptSessionPayload(payload));
}


// Public key shape
[Test]
public void PublicKeyBytes_IsUncompressedEcPoint_65Bytes()
{
// Arrange
var session = new MobileWalletAdapterSession();

// Act
byte[] pubKeyBytes = session.PublicKeyBytes;

// Assert
Assert.AreEqual(65, pubKeyBytes.Length,
"PublicKeyBytes must be 65 bytes (uncompressed EC point: 0x04 || X || Y)");
Assert.AreEqual((byte)0x04, pubKeyBytes[0],
"First byte of PublicKeyBytes must be 0x04 (uncompressed point marker)");
}

[Test]
public void TwoSessions_HaveDifferent_AssociationTokens()
{
// Each session should generate its own keypair.
var session1 = new MobileWalletAdapterSession();
var session2 = new MobileWalletAdapterSession();

// Assert
Assert.AreNotEqual(session1.AssociationToken, session2.AssociationToken,
"Two independent sessions must produce different AssociationTokens");
}
}
}
2 changes: 2 additions & 0 deletions Tests/EditMode/Crypto/MobileWalletAdapterSessionTests.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions Tests/EditMode/EditMode.asmdef
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "Solana.Unity.SDK.Tests.EditMode",
"rootNamespace": "Solana.Unity.SDK.Tests.EditMode",
"references": [
"com.solana.unity_sdk",
"UnityEngine.TestRunner",
"UnityEditor.TestRunner"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": true,
"precompiledReferences": [
"nunit.framework.dll",
"BouncyCastle.Cryptography.dll",
"Newtonsoft.Json.dll"
],
"autoReferenced": false,
"defineConstraints": [],
"noEngineReferences": false
}
7 changes: 7 additions & 0 deletions Tests/EditMode/EditMode.asmdef.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions Tests/EditMode/JsonRpc.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading