Add tests and refactor session management

This commit is contained in:
netkas 2024-10-24 14:06:39 -04:00
parent d4eb083d77
commit 1d0cd21965
16 changed files with 447 additions and 92 deletions

View file

@ -1,4 +1,79 @@
package net.nosial.socialclient; package net.nosial.socialclient;
public class Client { import net.nosial.socialclient.abstracts.RpcResult;
import net.nosial.socialclient.classes.Cryptography;
import net.nosial.socialclient.classes.RpcClient;
import net.nosial.socialclient.classes.Utilities;
import net.nosial.socialclient.exceptions.CryptographyException;
import net.nosial.socialclient.exceptions.ResolutionException;
import net.nosial.socialclient.exceptions.RpcException;
import net.nosial.socialclient.objects.RpcRequest;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.HashMap;
import java.util.Map;
public class Client extends RpcClient
{
protected Client(String domain) throws ResolutionException, CryptographyException
{
super(domain);
}
public Client(String endpoint, PublicKey serverPublicKey)
{
super(endpoint, serverPublicKey);
}
public String createSession(KeyPair keyPair) throws RpcException
{
RpcResult response = this.sendRequest(new RpcRequest("create_session", Utilities.randomCrc32(), new HashMap<>(){{
put("public_key", Cryptography.exportPublicKey(keyPair));
}}));
if(response == null)
{
throw new RpcException("Response is null");
}
if(!response.isSuccess())
{
throw new RpcException(response);
}
return (String) response.getResponse().getResult();
}
public void setSession(String sessionUuid, PrivateKey privateKey)
{
this.sessionUuid = sessionUuid;
this.privateKey = privateKey;
}
public String createAndSetSession(KeyPair keyPair) throws RpcException
{
String sessionUuid = this.createSession(keyPair);
this.setSession(sessionUuid, keyPair.getPrivate());
return sessionUuid;
}
public boolean ping() throws RpcException
{
RpcResult response = this.sendRequest(new RpcRequest("ping", Utilities.randomCrc32()));
if(response == null)
{
throw new RpcException("Response is null");
}
if(!response.isSuccess())
{
throw new RpcException(response);
}
return true;
}
} }

View file

@ -7,6 +7,7 @@ import javax.crypto.BadPaddingException;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException; import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException; import javax.crypto.NoSuchPaddingException;
import java.nio.charset.StandardCharsets;
import java.security.*; import java.security.*;
import java.security.spec.InvalidKeySpecException; import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.PKCS8EncodedKeySpec;
@ -79,15 +80,15 @@ public final class Cryptography
*/ */
public static String signContent(String content, PrivateKey privateKey) throws CryptographyException public static String signContent(String content, PrivateKey privateKey) throws CryptographyException
{ {
// Convert content to sha1 hash
try try
{ {
final Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM); Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
signature.initSign(privateKey); signature.initSign(privateKey);
signature.update(MessageDigest.getInstance("SHA-1").digest(content.getBytes())); signature.update(sha1(content).getBytes(StandardCharsets.UTF_8));
byte[] signatureBytes = signature.sign();
return Base64.getEncoder().encodeToString(signature.sign()); // Base64 encode the signature
return Base64.getEncoder().encodeToString(signatureBytes);
} }
catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e)
{ {
@ -95,6 +96,22 @@ public final class Cryptography
} }
} }
private static String bytesToHex(byte[] hash)
{
StringBuilder hexString = new StringBuilder(2 * hash.length);
for (byte b : hash)
{
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1)
{
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
}
/** /**
* Verifies the content using the provided signature and public key. * Verifies the content using the provided signature and public key.
* *
@ -123,9 +140,8 @@ public final class Cryptography
try try
{ {
final Signature sign = Signature.getInstance(SIGNATURE_ALGORITHM); final Signature sign = Signature.getInstance(SIGNATURE_ALGORITHM);
sign.initVerify(publicKey); sign.initVerify(publicKey);
sign.update(MessageDigest.getInstance("SHA-1").digest(content.getBytes())); sign.update(sha1(content).getBytes(StandardCharsets.UTF_8));
return sign.verify(Base64.getDecoder().decode(signature)); return sign.verify(Base64.getDecoder().decode(signature));
} }
@ -372,7 +388,7 @@ public final class Cryptography
X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
return getKeyFactory().generatePublic(spec); return getKeyFactory().generatePublic(spec);
} }
catch (InvalidKeySpecException | NoSuchAlgorithmException e) catch (InvalidKeySpecException | NoSuchAlgorithmException | IllegalArgumentException e)
{ {
throw new CryptographyException("Failed to import public key", e); throw new CryptographyException("Failed to import public key", e);
} }
@ -399,4 +415,11 @@ public final class Cryptography
} }
} }
public static String sha1(String data) throws NoSuchAlgorithmException
{
MessageDigest digest = MessageDigest.getInstance("SHA-1");
byte[] hash = digest.digest(data.getBytes(StandardCharsets.UTF_8));
return bytesToHex(hash);
}
} }

View file

@ -6,89 +6,54 @@ import net.nosial.socialclient.objects.ResolvedServer;
import javax.naming.NamingException; import javax.naming.NamingException;
import javax.naming.directory.*; import javax.naming.directory.*;
import java.util.Hashtable; import java.util.Hashtable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
// Improved Resolver class public class Resolver {
public class Resolver public static ResolvedServer resolveDomain(String domain) throws ResolutionException {
{ final Pattern fullPattern = Pattern.compile("v=socialbox;sb-rpc=(https?://[^;]+);sb-key=([^;]+)");
/**
* Resolves the domain to obtain endpoint and public key from its DNS TXT records.
*
* @param domain The domain to be resolved.
* @return ResolvedServer An instance of ResolvedServer containing the endpoint and public key.
* @throws ResolutionException If the DNS TXT records cannot be resolved or if required information is missing.
*/
public static ResolvedServer resolveDomain(String domain) throws ResolutionException
{
final String endpointPrefix = "socialbox=";
final String keyPrefix = "socialbox-key=";
// Disable caching
Hashtable<String, String> env = new Hashtable<>(); Hashtable<String, String> env = new Hashtable<>();
env.put("com.sun.jndi.dns.timeout.retries", "1"); env.put("com.sun.jndi.dns.timeout.retries", "1");
env.put("com.sun.jndi.dns.cache.ttl", "0"); env.put("com.sun.jndi.dns.cache.ttl", "1");
env.put("com.sun.jndi.dns.cache.negative.ttl", "0"); env.put("com.sun.jndi.dns.cache.negative.ttl", "0");
try try {
{
DirContext dirContext = new InitialDirContext(env); DirContext dirContext = new InitialDirContext(env);
Attributes attributes = dirContext.getAttributes("dns:/" + domain, new String[]{"TXT"}); Attributes attributes = dirContext.getAttributes("dns:/" + domain, new String[]{"TXT"});
Attribute attributeTXT = attributes.get("TXT"); Attribute attributeTXT = attributes.get("TXT");
if (attributeTXT == null) if (attributeTXT == null) {
{
throw new ResolutionException("Failed to resolve DNS TXT records for " + domain); throw new ResolutionException("Failed to resolve DNS TXT records for " + domain);
} }
String endpoint = null; StringBuilder fullRecordBuilder = new StringBuilder();
StringBuilder publicKeyBuilder = new StringBuilder();
boolean publicKeyFound = false;
for (int i = 0; i < attributeTXT.size(); i++) for (int i = 0; i < attributeTXT.size(); i++) {
{
String value = (String) attributeTXT.get(i); String value = (String) attributeTXT.get(i);
fullRecordBuilder.append(value.replaceAll("^\"|\"$", "").trim());
}
// Remove surrounding quotes if present String fullRecord = fullRecordBuilder.toString();
if (value.startsWith("\"") && value.endsWith("\"")) Matcher matcher = fullPattern.matcher(fullRecord);
{
value = value.substring(1, value.length() - 1); if (matcher.find()) {
String endpoint = matcher.group(1);
String publicKey = matcher.group(2).replaceAll("\\s+", "");
if (endpoint == null || endpoint.isEmpty()) {
throw new ResolutionException("Failed to resolve RPC endpoint for " + domain);
} }
// Split the value into fragments to find the relevant keys if (publicKey == null || publicKey.isEmpty()) {
String[] fragments = value.split("\\s+"); throw new ResolutionException("Failed to resolve public key for " + domain);
for (String fragment : fragments)
{
if (fragment.startsWith(endpointPrefix))
{
endpoint = fragment.substring(endpointPrefix.length());
}
else if (fragment.startsWith(keyPrefix))
{
publicKeyBuilder.append(fragment.substring(keyPrefix.length()));
publicKeyFound = true;
}
else if (publicKeyFound)
{
// If the public key has already started, append the fragment
publicKeyBuilder.append(fragment);
}
} }
}
if (endpoint == null) return new ResolvedServer(endpoint, publicKey);
{ } else {
throw new ResolutionException("Failed to resolve RPC endpoint for " + domain); throw new ResolutionException("Failed to find valid SocialBox record for " + domain);
} }
} catch (NamingException e) {
if (publicKeyBuilder.isEmpty())
{
throw new ResolutionException("Failed to resolve public key for " + domain);
}
String publicKey = publicKeyBuilder.toString();
return new ResolvedServer(endpoint, publicKey);
}
catch (NamingException e)
{
throw new ResolutionException("Error resolving domain: " + e.getMessage(), e); throw new ResolutionException("Error resolving domain: " + e.getMessage(), e);
} }
} }

View file

@ -12,6 +12,7 @@ import net.nosial.socialclient.objects.ResolvedServer;
import net.nosial.socialclient.objects.RpcRequest; import net.nosial.socialclient.objects.RpcRequest;
import java.io.IOException; import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey; import java.security.PrivateKey;
import java.security.PublicKey; import java.security.PublicKey;
import java.util.ArrayList; import java.util.ArrayList;
@ -30,8 +31,8 @@ public class RpcClient
private final PublicKey serverPublicKey; private final PublicKey serverPublicKey;
private final OkHttpClient httpClient; private final OkHttpClient httpClient;
private String sessionUuid; protected String sessionUuid;
private PrivateKey privateKey; protected PrivateKey privateKey;
/** /**
* Initializes a new instance of the RpcClient class using the specified domain. * Initializes a new instance of the RpcClient class using the specified domain.
@ -118,7 +119,7 @@ public class RpcClient
* @param privateKey the private key to be imported * @param privateKey the private key to be imported
* @throws CryptographyException if an error occurs while importing the private key * @throws CryptographyException if an error occurs while importing the private key
*/ */
public void setSession(String sessionUuid, String privateKey) throws CryptographyException protected void setSession(String sessionUuid, String privateKey) throws CryptographyException
{ {
this.sessionUuid = sessionUuid; this.sessionUuid = sessionUuid;
this.privateKey = Cryptography.importPrivateKey(privateKey); this.privateKey = Cryptography.importPrivateKey(privateKey);
@ -130,7 +131,7 @@ public class RpcClient
* @param sessionUuid the UUID of the session * @param sessionUuid the UUID of the session
* @param privateKey the private key associated with the session * @param privateKey the private key associated with the session
*/ */
public void setSession(String sessionUuid, PrivateKey privateKey) protected void setSession(String sessionUuid, PrivateKey privateKey)
{ {
this.sessionUuid = sessionUuid; this.sessionUuid = sessionUuid;
this.privateKey = privateKey; this.privateKey = privateKey;
@ -165,6 +166,7 @@ public class RpcClient
if(sessionUuid != null) if(sessionUuid != null)
{ {
// We are seeing the session UUID correctly.
this.addHeader("Session-UUID", sessionUuid); this.addHeader("Session-UUID", sessionUuid);
} }
@ -172,9 +174,12 @@ public class RpcClient
{ {
try try
{ {
// NOTE: Signature looks okay, it's base64 encoded.
System.out.println("Hash: " + Cryptography.sha1(jsonData));
System.out.println("Signature: " + Cryptography.signContent(jsonData, privateKey));
this.addHeader("Signature", Cryptography.signContent(jsonData, privateKey)); this.addHeader("Signature", Cryptography.signContent(jsonData, privateKey));
} }
catch(CryptographyException e) catch(CryptographyException | NoSuchAlgorithmException e)
{ {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
@ -193,7 +198,7 @@ public class RpcClient
throw new RuntimeException(responseString); throw new RuntimeException(responseString);
} }
throw new RuntimeException("Failed to send request"); throw new RuntimeException("Failed to send request: " + response.code());
} }
if(response.code() == 204) if(response.code() == 204)
@ -242,7 +247,14 @@ public class RpcClient
*/ */
public RpcResult sendRequest(RpcRequest request) public RpcResult sendRequest(RpcRequest request)
{ {
return sendRequest(encode(request.toMap())).getFirst(); List<RpcResult> results = sendRequest(encode(request.toMap()));
if(results.isEmpty())
{
return null;
}
return results.getFirst();
} }
/** /**

View file

@ -0,0 +1,44 @@
package net.nosial.socialclient.classes;
import java.util.concurrent.ThreadLocalRandom;
public class Utilities
{
private static final int[] CRC32_TABLE = new int[256];
static
{
for (int i = 0; i < 256; i++)
{
int crc = i;
for (int j = 8; j > 0; j--)
{
if ((crc & 1) == 1)
{
crc = (crc >>> 1) ^ 0xEDB88320;
}
else
{
crc = crc >>> 1;
}
}
CRC32_TABLE[i] = crc;
}
}
public static String randomCrc32()
{
long randomValue = ThreadLocalRandom.current().nextLong(Long.MAX_VALUE);
String valueString = Long.toString(randomValue);
int crc = 0xFFFFFFFF;
for (char c : valueString.toCharArray())
{
crc = (crc >>> 8) ^ CRC32_TABLE[(crc ^ c) & 0xFF];
}
crc = ~crc;
return Integer.toHexString(crc);
}
}

View file

@ -1,6 +1,21 @@
package net.nosial.socialclient.exceptions; package net.nosial.socialclient.exceptions;
import net.nosial.socialclient.abstracts.RpcResult;
public class RpcException extends Exception public class RpcException extends Exception
{ {
public RpcException(String message)
{
super(message);
}
public RpcException(String message, Throwable cause)
{
super(message, cause);
}
public RpcException(RpcResult result)
{
super(result.getErrorResponse().getError());
}
} }

View file

@ -7,6 +7,11 @@ public class ResolvedServer
public ResolvedServer(String endpoint, String publicKey) public ResolvedServer(String endpoint, String publicKey)
{ {
if(endpoint == null || publicKey == null)
{
throw new IllegalArgumentException("Endpoint and public key must not be null");
}
this.endpoint = endpoint; this.endpoint = endpoint;
this.publicKey = publicKey; this.publicKey = publicKey;
} }

View file

@ -7,7 +7,7 @@ public class RpcRequest
{ {
private final String method; private final String method;
private final String id; private final String id;
private final Map<String, Object> params; private final Map<String, Object> parameters;
/** /**
* Initializes a new RpcRequest object using the provided data map. * Initializes a new RpcRequest object using the provided data map.
@ -23,7 +23,7 @@ public class RpcRequest
{ {
this.method = (String) data.get("method"); this.method = (String) data.get("method");
this.id = (String) data.get("id"); this.id = (String) data.get("id");
this.params = (Map<String, Object>) data.get("params"); this.parameters = (Map<String, Object>) data.get("params");
} }
/** /**
@ -31,13 +31,13 @@ public class RpcRequest
* *
* @param method the RPC method to be called * @param method the RPC method to be called
* @param id the unique identifier for this request * @param id the unique identifier for this request
* @param params the parameters for the RPC method * @param parameters the parameters for the RPC method
*/ */
public RpcRequest(String method, String id, Map<String, Object> params) public RpcRequest(String method, String id, Map<String, Object> parameters)
{ {
this.method = method; this.method = method;
this.id = id; this.id = id;
this.params = params; this.parameters = parameters;
} }
/** /**
@ -45,13 +45,13 @@ public class RpcRequest
* The request ID will be set to null. * The request ID will be set to null.
* *
* @param method the name of the method being called * @param method the name of the method being called
* @param params the parameters to be sent with the method call * @param parameters the parameters to be sent with the method call
*/ */
public RpcRequest(String method, Map<String, Object> params) public RpcRequest(String method, Map<String, Object> parameters)
{ {
this.id = null; this.id = null;
this.method = method; this.method = method;
this.params = params; this.parameters = parameters;
} }
/** /**
@ -64,7 +64,7 @@ public class RpcRequest
{ {
this.method = method; this.method = method;
this.id = id; this.id = id;
this.params = null; this.parameters = null;
} }
/** /**
@ -76,7 +76,7 @@ public class RpcRequest
{ {
this.id = null; this.id = null;
this.method = method; this.method = method;
this.params = null; this.parameters = null;
} }
/** /**
@ -104,9 +104,9 @@ public class RpcRequest
* *
* @return a Map containing the parameters of the RPC request. * @return a Map containing the parameters of the RPC request.
*/ */
public Map<String, Object> getParams() public Map<String, Object> getParameters()
{ {
return this.params; return this.parameters;
} }
/** /**
@ -124,9 +124,9 @@ public class RpcRequest
map.put("id", this.id); map.put("id", this.id);
} }
if(this.params != null) if(this.parameters != null)
{ {
map.put("params", this.params); map.put("parameters", this.parameters);
} }
return map; return map;

View file

@ -0,0 +1,89 @@
package net.nosial.socialclient;
import net.nosial.socialclient.classes.Cryptography;
import net.nosial.socialclient.exceptions.CryptographyException;
import net.nosial.socialclient.exceptions.ResolutionException;
import net.nosial.socialclient.exceptions.RpcException;
import org.junit.jupiter.api.Test;
import java.security.KeyPair;
import static org.junit.jupiter.api.Assertions.*;
class SessionTest
{
@Test
void createSessionTest()
{
Client socialClient;
KeyPair keyPair;
String sessionUuid;
try
{
socialClient = new Client("n64.cc");
}
catch (ResolutionException | CryptographyException e)
{
fail(e.getMessage(), e);
return;
}
try
{
keyPair = Cryptography.generateKeyPair();
}
catch (CryptographyException e)
{
fail(e.getMessage(), e);
return;
}
try
{
sessionUuid = socialClient.createSession(keyPair);
}
catch (RpcException e)
{
fail(e.getMessage(), e);
return;
}
assertNotNull(sessionUuid);
System.out.println("Session UUID: " + sessionUuid);
}
@Test
public void pingSignedTest()
{
final Client socialClient;
final KeyPair keyPair;
final String sessionUuid;
try
{
socialClient = new Client("n64.cc");
keyPair = Cryptography.generateKeyPair();
sessionUuid = socialClient.createAndSetSession(keyPair);
}
catch (ResolutionException | CryptographyException | RpcException e)
{
fail(e.getMessage(), e);
return;
}
System.out.println("Session UUID: " + sessionUuid);
assertNotNull(sessionUuid);
try
{
assertTrue(socialClient.ping());
}
catch (RpcException e)
{
fail(e.getMessage(), e);
}
}
}

View file

@ -4,8 +4,16 @@ import net.nosial.socialclient.exceptions.CryptographyException;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.security.KeyPair; import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Collections; import java.util.Collections;
import java.util.Objects;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
@ -259,4 +267,78 @@ class CryptographyTest
// Assert // Assert
assertThrows(CryptographyException.class, () -> Cryptography.encrypt(content, invalidPublicKey), "Encrypt should throw CryptographyException when public key is invalid"); assertThrows(CryptographyException.class, () -> Cryptography.encrypt(content, invalidPublicKey), "Encrypt should throw CryptographyException when public key is invalid");
} }
@Test
public void clientSignTest() {
ClassLoader classLoader = getClass().getClassLoader();
PrivateKey privateKey;
PublicKey publicKey;
String content;
try
{
privateKey = Cryptography.importPrivateKey(Files.readString(new File(Objects.requireNonNull(classLoader.getResource("client_private.der")).getFile()).toPath()));
publicKey = Cryptography.importPublicKey(Files.readString(new File(Objects.requireNonNull(classLoader.getResource("client_public.der")).getFile()).toPath()));
content = Files.readString(new File(Objects.requireNonNull(classLoader.getResource("content.txt")).getFile()).toPath());
}
catch(CryptographyException | IOException e)
{
fail(e.getMessage(), e);
return;
}
assertNotNull(privateKey, "Private key should not be null");
assertNotNull(publicKey, "Public key should not be null");
assertNotNull(content, "Content should not be null");
// Hash the content to sha1 string
String sha1Content;
try
{
sha1Content = Cryptography.sha1(content);
}
catch (NoSuchAlgorithmException e)
{
fail(e.getMessage(), e);
return;
}
assertNotNull(sha1Content, "SHA1 content should not be null");
assertEquals(40, sha1Content.length(), "SHA1 content should be 40 characters length");
assertEquals("fa2415f0735a8aa151195688852178e8fd6e77c5", sha1Content, "SHA1 content should be equal to expected value");
System.out.println("SHA1 content: " + sha1Content);
// Signature testing
String signature;
try
{
signature = Cryptography.signContent(content, privateKey);
}
catch (CryptographyException e)
{
fail(e.getMessage(), e);
return;
}
assertNotNull(signature, "Signature should not be null");
assertEquals("Gcnijq7V8AYXgdk/eP9IswXN7831FevlBNDTKN60Ku7xesPDuPX8e55+38WFGCQ87DbeiIr+61XIDoN4+bTM4Wl0YSUe7oHV9BBnBqGhyZTntDPedUYUomrF3IRcpVRK0SbQSRaYucIp/ZsSHdbQgQBtDCvH5pK1+5g+VK9ZFT16Isvk0PhMjZiLkUYxUklFuzak7agWiS3wllFPqYSM6ri0RF+5I5JbnR9fUAOfhOceax//5H7d2WsdLj6DwtuY+eL5WyHxSmGA04YeQF3JgOGJ3WX2DSH8L0zA7pkGOjz5y1Nu6+0U6KRUXcezU/iM4zy5OJOnD5eJH4pYZizkiA==", signature, "Signature should be equal to expected value");
System.out.println("Signature: " + signature);
// Verify the signature
boolean verified;
try
{
verified = Cryptography.verifyContent(content, signature, publicKey);
}
catch (CryptographyException e)
{
fail(e.getMessage(), e);
return;
}
assertTrue(verified, "Signature should be verified successfully");
}
} }

View file

@ -0,0 +1,37 @@
package net.nosial.socialclient.classes;
import net.nosial.socialclient.classes.Utilities;
import org.junit.jupiter.api.Test;
import java.util.regex.Pattern;
import java.util.HashSet;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.*;
public class UtilitiesTest {
@Test
public void testRandomCrc32_formatValidity() {
String crcOutput = Utilities.randomCrc32();
assertNotNull(crcOutput);
assertTrue(crcOutput.matches("[a-f0-9]+"), "Expected hexadecimal output");
}
@Test
public void testRandomCrc32_lengthValidity() {
String crcOutput = Utilities.randomCrc32();
assertEquals(8, crcOutput.length(), "Expected 8 characters length");
}
@Test
public void testRandomCrc32_randomness() {
Set<String> randomResults = new HashSet<>();
for (int i = 0; i < 10_000; i++) {
randomResults.add(Utilities.randomCrc32());
}
assertTrue(randomResults.size() > 1, "Expected more than one different results");
}
}

View file

@ -0,0 +1 @@
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDZb4K+28kKYe1CvHPWHJALJFS396HOFmBv+anpAVWMDGBUyAWbWEqxmTAV17cBHiICjDDCNFBpOZLWzIiUpdWKA0Jo+Vu9zgWSPUyGe/Lik4GFNZ38gfolfdKGnLNFnn4nFR/fsZQ7hg4wWDarJmhJ+ZSLShOz2uIb4LaKk2qy12c6Zepufgrbk9TwWZQiXkzqBWbrZDpw0pp50CzoIwEnYJ+a7vhb98jpeS+Jjnp5zWlFjv9RgzOQUOwwOK4We2gNAVeFC5BP9trklpTh1bJlit4CECH68fCGjgoTOU92UbgucgyA4O5FVPGQYPAMuiZMGFaqXE2E7z1XwYIMAL4VAgMBAAECggEAAKiJz3CYuO+gGnL+F7qjaSXCUE8VvPfoCwuNYHNEFXo9DJBmnL7EU2WrYG+wARCP7O7qd0dEidx9u36ytjyCcKT4nYni8lM1zU7rVvbnLbsuRZS/4RO/RaYfPxig94fDfSeJ2ma0i7G56onj+MBbyTZarZ7Bf8hpcmKg9pkNEcEVcklNIwwbXKBOGq75Vka/+W56JZKJD3G9YmfrAO5RGF1prh93MRXlxlN/91k/m2pqkN9xYofepn0ePmI8Ci18jrMpJbmeu8BkypzgvC/5EfHipn7y/yJ215o/EtB575muz2zngRXe+GVO5lB5d5PuEwmXoaV5o3BqkIcb3aiz4QKBgQD7P1AE2/3oATNUF1FwlXzvdCS7M2BB28jQWjzJvHus1d1+qA2StWPgCPG2D/YTtHPI3xefBnAmeSIFCFEub0YLONbRvtQAZdTt5SAaZuUyMprqD1sCUHCizyVO0wHxo3DS0sIFmo/Lpc+jnYHn3KcuRPRJk3ncZNCQhy9a/rrnxQKBgQDdjHY82YdkWQWj/xM1EuVtkVVeCJWJ6tSDn+Uq8d+hXILFAQ47GOUbzj4Ty4qGgsAgsaAGqja5t6CE+fYs8Q34FsxTsYgIRm0VXqtPm4aYTQ4PwKbmMPEOgEsXBywe5Y+QB0u/WuNyhgwgYP5cy1IS3HA1HmbTisi0zLEfkVWSEQKBgCuP36zoA88NHjwvStSNZrsR1SiMEN16YQgXDUEhKARglGXYd3n/b1Cx3E7n14+1Evo6DBtrf1h8WjSrK4A0lN1vPnfhcVqcTV3uAzHwsz6P3aJFhU8SaWUhK2POXCDsaKx1FGTqVpJFrom8zoBIFsiD9iMnqdJXvH3CoqhRUFDNAoGAEJdwU2ZHCXDRR1LW8WaU3/u+VOh3qnh3qdPTqb+ra74t3OsTUcGvhsGPTJQ1r5UjJk+nGFiu+IGT9+FwWjVDQo0SiEIHWfdMPAl28uNG1SkQIIXg+eQ4aUmaVgMnfrjaY4LoXVBFMFJxngslgXWIk/kGPjQkpzsBhOi/awnLSsECgYEAkSEb3CXfq1r/8qXMTzI+A9CGPr++aC2H6ytFNGJq4J+P40u1tcsfkwNGcQ0Hp+Qz3FHBYFuMxtjXDq0QSvVKEhdV9bjlZhTqN3lqWcCukU3ESqRbxsIj9izuncpxSP7G19WEU0anGD9ev+QWYdHPTBY9nn1+H0tkJjqh4XkRBuY=

View file

@ -0,0 +1 @@
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2W+CvtvJCmHtQrxz1hyQCyRUt/ehzhZgb/mp6QFVjAxgVMgFm1hKsZkwFde3AR4iAowwwjRQaTmS1syIlKXVigNCaPlbvc4Fkj1Mhnvy4pOBhTWd/IH6JX3ShpyzRZ5+JxUf37GUO4YOMFg2qyZoSfmUi0oTs9riG+C2ipNqstdnOmXqbn4K25PU8FmUIl5M6gVm62Q6cNKaedAs6CMBJ2Cfmu74W/fI6XkviY56ec1pRY7/UYMzkFDsMDiuFntoDQFXhQuQT/ba5JaU4dWyZYreAhAh+vHwho4KEzlPdlG4LnIMgODuRVTxkGDwDLomTBhWqlxNhO89V8GCDAC+FQIDAQAB

View file

@ -0,0 +1,4 @@
{
"method" : "ping",
"id" : "daa31852"
}

View file

@ -0,0 +1 @@
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDxcYLVvKYHpxJQe49Z7boilJfmp/uYAr4rQNN2El9nPG3hVRYIsmhzIeByU7ZJLn0EG5D2G6T4SlydfbHbHK3NMciTgmyAZRJl10z/KIlPG1n6DaK5Oo6VT9ty+uI7JKFdQQwzSrPP2u4KNERqK4vtfsxdiAcMXS/hUncID4ARvmigmcTOTcosdH57axSqf1xfJJ12zl3QPb6wppsBAJrZ811Ll8eZYhFoQwe0oE3T5Q0aqK0Ecgh4cYhF63nEAKkcPOMxkhcZWpr3YzqD7Rj9VAzk7xRM/QJ1SjsVwdB+YCQK1tbTloTY3BqtEib9ErZvjlaB26GZbV32kNsPnwHXAgMBAAECggEAJBVtUskzXRBsjcexmGSNfW6MtyWi1ciPKEKzd8FuLa0b1OHU/a7AKnjFJQD6zLwcZflCtG1UPeFLLyRiaNdD6FdI3TbQRW4Vjk/bi4TA5Kg3TcYs7BbiyVDagLgbCHDEhv3aN24yKl3TVoYSNXXVn0RkgZP7Ta89oSSkcnlyj/QFOA8RfIm5q+qiAPvOqFf8NKlm0hZDrxWHG/OduYHq25S9ohNzymyM+1CYTrVFZCTfscDvLBDd2MVpNRyxoQquiMlfIEUBGlu9uFWy9Hovv8Sd3irgvcBtjL8iPaMzJe3p6T83KL2AgXHcYT7r9Vlvqib5x1iTYvlid25zzQ19IQKBgQD1BISfPugEp+fAdoGHOygG+gzNE8/1ldhnA9bTCZZ3FQBTI2lPRZBFDKuirc6glbCHiWrd8HoJ3BO3kbGzq4EDBf0VDFby/7nkrroTW+RIn+THlfciWgjSATGgCPOHmvM6JmIpuYsbKkdmV4ITVWwvLPxDAwlMnHyJOuYTj8xJ4QKBgQD8Q/rdWoRBSVCrDb0QO/Or5FAJELmYFtFWBBCEpadr9ci0e/mSbbZlXjP98m4XesIIRpcpG3gU8P3hKB7H60ynPN6Jyw33YhIlJHaEjYISN/h5Vw0ybQkyFR9CBRjOp59CBcb8AsdA/OQjxFz8h46PbPLCWCR5kM3tKbuNobBytwKBgQC4Rr+gLWW/KrEolXhxxtIh/SpniwEbSanKQJ7vdgSOZ2MpJDbuAfmxlQf5gBMpv6tXJMkVRuniRH0n0RH/eXu8VGK10+QJOsAK+EbGjJQy8t7UJTwLv/9mQrOaE2FlmepYz8mAbCXtNm0g0avo8pQ9Hu5TUBNMZV1csMmd6MbSwQKBgQDit+X6kqNSWaXaVdqZgIga8HLN8u4aNkelWrnNvWOer6LWMqW2aEwJBoULsponF/jSnz6zfzCJAZ3qgbhITLzzgM0wYgIHV2ifYQnzT4qa/RqfUxFVRJGDJWCWYSZOdG+5Up/nVkflrGMNkilP/DSvymbTK4x8hRvODje1rp96OQKBgFmXLpHPN8WAXP7VVyb3RqYYRgtxXjY2yj/CYwnXl0k4Uji08S9Ke2AqljiSzmZs1Wh1UBLap90F0smRVHmYgwl2rPjNiXbyKd4W9R4vEVYgEmcnvzba107o76qFmEbyW/K7a7K/jKaH8KAytgR/cHd+SIBctcDv8uKmZ1MJT9p8

View file

@ -0,0 +1 @@
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8XGC1bymB6cSUHuPWe26IpSX5qf7mAK+K0DTdhJfZzxt4VUWCLJocyHgclO2SS59BBuQ9huk+EpcnX2x2xytzTHIk4JsgGUSZddM/yiJTxtZ+g2iuTqOlU/bcvriOyShXUEMM0qzz9ruCjREaiuL7X7MXYgHDF0v4VJ3CA+AEb5ooJnEzk3KLHR+e2sUqn9cXySdds5d0D2+sKabAQCa2fNdS5fHmWIRaEMHtKBN0+UNGqitBHIIeHGIRet5xACpHDzjMZIXGVqa92M6g+0Y/VQM5O8UTP0CdUo7FcHQfmAkCtbW05aE2NwarRIm/RK2b45WgduhmW1d9pDbD58B1wIDAQAB