Skip to content

Commit

Permalink
Add initial ZK login implementation. #119
Browse files Browse the repository at this point in the history
  • Loading branch information
kPatch committed Nov 14, 2024
1 parent b4185ee commit 0343230
Show file tree
Hide file tree
Showing 7 changed files with 270 additions and 0 deletions.
8 changes: 8 additions & 0 deletions Assets/Sui-Unity-SDK/Code/Sui.ZKLogin/SDK.meta

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

121 changes: 121 additions & 0 deletions Assets/Sui-Unity-SDK/Code/Sui.ZKLogin/SDK/Address.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
using System;
using System.Numerics;
using System.Security.Cryptography;
using System.Text;
using Jose;


namespace Sui.ZKLogin.SDK
{
public static class Address
{
public const int MAX_HEADER_LEN_B64 = 248;
public const int MAX_PADDED_UNSIGNED_JWT_LEN = 64 * 25;

private static readonly char[] HexChars = "0123456789abcdef".ToCharArray();

private static string BytesToHex(byte[] bytes)
{
char[] hex = new char[bytes.Length * 2 + 2];
hex[0] = '0';
hex[1] = 'x';

for (int i = 0; i < bytes.Length; i++)
{
hex[i * 2 + 2] = HexChars[bytes[i] >> 4];
hex[i * 2 + 3] = HexChars[bytes[i] & 0xF];
}
return new string(hex);
}

public static void LengthChecks(string jwt)
{
var parts = jwt.Split(".");
string header = parts[0];

if (header.Length > MAX_HEADER_LEN_B64)
throw new ArgumentException("Header is too long");

int L = (header.Length + 1 + parts[1].Length) * 8;
int K = (512 + 448 - ((L % 512) + 1)) % 512;
int paddedUnsignedJwtLen = (L + 1 + K + 64) / 8;

if (paddedUnsignedJwtLen > MAX_PADDED_UNSIGNED_JWT_LEN)
throw new ArgumentException("JWT is too long");
}

public static string JwtToAddress(string jwt, string userSalt)
{
LengthChecks(jwt);

var payload = JWT.Decode<JwtPayload>(jwt);
if (string.IsNullOrEmpty(payload.Sub) || string.IsNullOrEmpty(payload.Iss) || string.IsNullOrEmpty(payload.Aud))
throw new ArgumentException("Missing jwt data");

// Check if Aud is an array by checking if it contains a comma
// This is a simple way to detect multiple audience values
if (payload.Aud.Contains(","))
throw new ArgumentException("Not supported aud. Aud is an array, string was expected.");

return ComputeZkLoginAddress(new ZkLoginAddressOptions
{
UserSalt = userSalt,
ClaimName = "sub",
ClaimValue = payload.Sub,
Aud = payload.Aud,
Iss = payload.Iss
});
}


public static string ComputeZkLoginAddress(ZkLoginAddressOptions options)
{
var seed = GenAddressSeed(options.UserSalt, options.ClaimName, options.ClaimValue, options.Aud);
return ComputeZkLoginAddressFromSeed(seed, options.Iss);
}

private static BigInteger GenAddressSeed(string userSalt, string claimName, string claimValue, string aud)
{
using var sha256 = SHA256.Create();
var saltBytes = Encoding.UTF8.GetBytes(userSalt);
var claimBytes = Encoding.UTF8.GetBytes($"{claimName}:{claimValue}:{aud}");

var combined = new byte[saltBytes.Length + claimBytes.Length];
Buffer.BlockCopy(saltBytes, 0, combined, 0, saltBytes.Length);
Buffer.BlockCopy(claimBytes, 0, combined, saltBytes.Length, claimBytes.Length);

var hash = sha256.ComputeHash(combined);
return new BigInteger(hash);
}

private static string ComputeZkLoginAddressFromSeed(BigInteger seed, string iss)
{
using var sha256 = SHA256.Create();
var issBytes = Encoding.UTF8.GetBytes(iss);
var seedBytes = seed.ToByteArray();

var combined = new byte[seedBytes.Length + issBytes.Length];
Buffer.BlockCopy(seedBytes, 0, combined, 0, seedBytes.Length);
Buffer.BlockCopy(issBytes, 0, combined, seedBytes.Length, issBytes.Length);

var hash = sha256.ComputeHash(combined);
return BytesToHex(hash);
}
}

public class ZkLoginAddressOptions
{
public string ClaimName { get; set; }
public string ClaimValue { get; set; }
public string UserSalt { get; set; }
public string Iss { get; set; }
public string Aud { get; set; }
}

public class JwtPayload
{
public string Sub { get; set; }
public string Iss { get; set; }
public string Aud { get; set; }
}
}
11 changes: 11 additions & 0 deletions Assets/Sui-Unity-SDK/Code/Sui.ZKLogin/SDK/Address.cs.meta

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

86 changes: 86 additions & 0 deletions Assets/Sui-Unity-SDK/Code/Sui.ZKLogin/SDK/Poseidon.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using System;
using System.Numerics;
using System.Linq;

namespace Sui.ZKLogin.SDK
{
public static class PoseidonHasher
{
public static readonly BigInteger BN254_FIELD_SIZE = BigInteger.Parse("21888242871839275222246405745257275088548364400416034343698204186575808495617");

private static readonly Func<BigInteger[], BigInteger>[] PoseidonNumToHashFN = {
Poseidon.Hash1,
Poseidon.Hash2,
Poseidon.Hash3,
Poseidon.Hash4,
Poseidon.Hash5,
Poseidon.Hash6,
Poseidon.Hash7,
Poseidon.Hash8,
Poseidon.Hash9,
Poseidon.Hash10,
Poseidon.Hash11,
Poseidon.Hash12,
Poseidon.Hash13,
Poseidon.Hash14,
Poseidon.Hash15,
Poseidon.Hash16
};

public static BigInteger PoseidonHash(object[] inputs)
{
var bigIntInputs = inputs.Select(x => {
var b = ToBigInteger(x);
if (b < 0 || b >= BN254_FIELD_SIZE)
throw new ArgumentException($"Element {b} not in the BN254 field");
return b;
}).ToArray();

if (bigIntInputs.Length <= 16 && PoseidonNumToHashFN.Length >= bigIntInputs.Length)
{
return PoseidonNumToHashFN[bigIntInputs.Length - 1](bigIntInputs);
}
else if (bigIntInputs.Length <= 32)
{
var hash1 = PoseidonHash(bigIntInputs.Take(16).Cast<object>().ToArray());
var hash2 = PoseidonHash(bigIntInputs.Skip(16).Cast<object>().ToArray());
return PoseidonHash(new object[] { hash1, hash2 });
}

throw new ArgumentException($"Unable to hash a vector of length {bigIntInputs.Length}");
}

private static BigInteger ToBigInteger(object input)
{
return input switch
{
BigInteger bi => bi,
int i => new BigInteger(i),
long l => new BigInteger(l),
string s => BigInteger.Parse(s),
_ => throw new ArgumentException($"Unsupported input type: {input.GetType()}")
};
}
}

// This interface needs to be implemented based on the poseidon-lite functionality
public static class Poseidon
{
public static BigInteger Hash1(BigInteger[] inputs) => throw new NotImplementedException();
public static BigInteger Hash2(BigInteger[] inputs) => throw new NotImplementedException();
public static BigInteger Hash3(BigInteger[] inputs) => throw new NotImplementedException();
public static BigInteger Hash4(BigInteger[] inputs) => throw new NotImplementedException();
public static BigInteger Hash5(BigInteger[] inputs) => throw new NotImplementedException();
public static BigInteger Hash6(BigInteger[] inputs) => throw new NotImplementedException();
public static BigInteger Hash7(BigInteger[] inputs) => throw new NotImplementedException();
public static BigInteger Hash8(BigInteger[] inputs) => throw new NotImplementedException();
public static BigInteger Hash9(BigInteger[] inputs) => throw new NotImplementedException();
public static BigInteger Hash10(BigInteger[] inputs) => throw new NotImplementedException();
public static BigInteger Hash11(BigInteger[] inputs) => throw new NotImplementedException();
public static BigInteger Hash12(BigInteger[] inputs) => throw new NotImplementedException();
public static BigInteger Hash13(BigInteger[] inputs) => throw new NotImplementedException();
public static BigInteger Hash14(BigInteger[] inputs) => throw new NotImplementedException();
public static BigInteger Hash15(BigInteger[] inputs) => throw new NotImplementedException();
public static BigInteger Hash16(BigInteger[] inputs) => throw new NotImplementedException();
}
}
11 changes: 11 additions & 0 deletions Assets/Sui-Unity-SDK/Code/Sui.ZKLogin/SDK/Poseidon.cs.meta

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

22 changes: 22 additions & 0 deletions Assets/Sui-Unity-SDK/Code/Sui.ZKLogin/SDK/Utils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;


namespace Sui.ZKLogin.SDK
{
public class Utils : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{

}

// Update is called once per frame
void Update()
{

}
}
}
11 changes: 11 additions & 0 deletions Assets/Sui-Unity-SDK/Code/Sui.ZKLogin/SDK/Utils.cs.meta

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

0 comments on commit 0343230

Please sign in to comment.