diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..919ce1f --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..a55e7a1 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..aa00ffa --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..9ea3508 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..6ddae43 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 0000000..2b63946 --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..7ae6270 --- /dev/null +++ b/pom.xml @@ -0,0 +1,47 @@ + + + 4.0.0 + + net.nosial + socialclient + 1.0-SNAPSHOT + + + 22 + 22 + UTF-8 + + + + + org.junit.jupiter + junit-jupiter-api + 5.8.1 + test + + + org.junit.jupiter + junit-jupiter-engine + 5.8.1 + test + + + com.fasterxml.jackson.core + jackson-databind + 2.15.2 + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + 2.15.2 + + + com.squareup.okhttp + okhttp + 2.7.5 + + + + \ No newline at end of file diff --git a/src/main/java/net/nosial/Main.java b/src/main/java/net/nosial/Main.java new file mode 100644 index 0000000..38dfc79 --- /dev/null +++ b/src/main/java/net/nosial/Main.java @@ -0,0 +1,17 @@ +package net.nosial; + +//TIP To Run code, press or +// click the icon in the gutter. +public class Main { + public static void main(String[] args) { + //TIP Press with your caret at the highlighted text + // to see how IntelliJ IDEA suggests fixing it. + System.out.printf("Hello and welcome!"); + + for (int i = 1; i <= 5; i++) { + //TIP Press to start debugging your code. We have set one breakpoint + // for you, but you can always add more by pressing . + System.out.println("i = " + i); + } + } +} \ No newline at end of file diff --git a/src/main/java/net/nosial/socialclient/Client.java b/src/main/java/net/nosial/socialclient/Client.java new file mode 100644 index 0000000..d4a10e3 --- /dev/null +++ b/src/main/java/net/nosial/socialclient/Client.java @@ -0,0 +1,4 @@ +package net.nosial.socialclient; + +public class Client { +} diff --git a/src/main/java/net/nosial/socialclient/abstracts/RpcResult.java b/src/main/java/net/nosial/socialclient/abstracts/RpcResult.java new file mode 100644 index 0000000..e1e6365 --- /dev/null +++ b/src/main/java/net/nosial/socialclient/abstracts/RpcResult.java @@ -0,0 +1,39 @@ +package net.nosial.socialclient.abstracts; + +import net.nosial.socialclient.objects.RpcError; +import net.nosial.socialclient.objects.RpcResponse; + +import java.util.Map; + +public abstract class RpcResult +{ + private final boolean success; + + protected RpcResult(boolean success) + { + this.success = success; + } + + public boolean isSuccess() + { + return this.success; + } + + public abstract RpcError getErrorResponse(); + public abstract RpcResponse getResponse(); + + public static RpcResult fromMap(Map data) + { + if(data.containsKey("error")) + { + return new RpcError(data); + } + else if(data.containsKey("id")) + { + return new RpcResponse(data); + } + + // TODO: Use standard RpcException for this or something + throw new IllegalArgumentException("Cannot recognize RPC object"); + } +} diff --git a/src/main/java/net/nosial/socialclient/classes/Cryptography.java b/src/main/java/net/nosial/socialclient/classes/Cryptography.java new file mode 100644 index 0000000..7f874a3 --- /dev/null +++ b/src/main/java/net/nosial/socialclient/classes/Cryptography.java @@ -0,0 +1,402 @@ +package net.nosial.socialclient.classes; + + +import net.nosial.socialclient.exceptions.CryptographyException; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import java.security.*; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; + +public final class Cryptography +{ + private static final int TIME_BLOCK = 60; + private static final int KEY_SIZE = 2048; + private static final String ALGORITHM = "RSA"; + private static final String SIGNATURE_ALGORITHM = "SHA256withRSA"; + private static final String CIPHER_TRANSFORMATION = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"; + private static final int MAX_ENCRYPT_BLOCK_SIZE = 214; + private static final int MAX_DECRYPT_BLOCK_SIZE = 256; + + /** + * Returns the KeyFactory object for the specified algorithm. + * + * @return the KeyFactory object for the specified algorithm + * @throws NoSuchAlgorithmException if the requested algorithm is not available + */ + private static KeyFactory getKeyFactory() throws NoSuchAlgorithmException + { + return KeyFactory.getInstance(ALGORITHM); + } + + /** + * Generates a key pair using the RSA algorithm. + * + * @return the generated key pair + * @throws CryptographyException if there is an error generating the key pair + */ + public static KeyPair generateKeyPair() throws CryptographyException + { + try + { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM); + keyPairGenerator.initialize(KEY_SIZE); + return keyPairGenerator.generateKeyPair(); + } + catch (NoSuchAlgorithmException e) + { + throw new CryptographyException("Failed to generate key pair, RSA algorithm not found", e); + } + } + + /** + * Signs the given content using the provided private key. + * + * @param content the content to be signed + * @param privateKey the private key used for signing + * @return the base64 encoded signature of the content + * @throws CryptographyException if an error occurs in the cryptography process + */ + public static String signContent(String content, String privateKey) throws CryptographyException + { + return signContent(content, importPrivateKey(privateKey)); + } + + /** + * Signs the given content using the provided private key. + * + * @param content the content to be signed + * @param privateKey the private key used for signing + * @return the base64 encoded signature of the content + * @throws CryptographyException if an error occurs in the cryptography process + */ + public static String signContent(String content, PrivateKey privateKey) throws CryptographyException + { + // Convert content to sha1 hash + try + { + final Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM); + + signature.initSign(privateKey); + signature.update(MessageDigest.getInstance("SHA-1").digest(content.getBytes())); + + return Base64.getEncoder().encodeToString(signature.sign()); + } + catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) + { + throw new CryptographyException("Failed to sign content", e); + } + } + + /** + * Verifies the content using the provided signature and public key. + * + * @param content the content to be verified + * @param signature the signature of the content + * @param publicKey the public key used for verification + * @return true if the content is verified, false otherwise + * @throws CryptographyException if an error occurs in the cryptography process + */ + public static boolean verifyContent(String content, String signature, String publicKey) throws CryptographyException + { + return verifyContent(content, signature, importPublicKey(publicKey)); + } + + /** + * Verifies the signature of the given content using the provided public key. + * + * @param content the content to be verified + * @param signature the signature to be verified + * @param publicKey the public key used for verification + * @return true if the signature is verified, false otherwise + * @throws CryptographyException if there is an error during the verification process + */ + public static boolean verifyContent(String content, String signature, PublicKey publicKey) throws CryptographyException + { + try + { + final Signature sign = Signature.getInstance(SIGNATURE_ALGORITHM); + + sign.initVerify(publicKey); + sign.update(MessageDigest.getInstance("SHA-1").digest(content.getBytes())); + + return sign.verify(Base64.getDecoder().decode(signature)); + } + catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) + { + throw new CryptographyException("Failed to verify content", e); + } + } + + /** + * Generates a temporary signature for the given content using the provided private key. + * + * @param content the content to be signed + * @param privateKey the private key used for signing + * @return the base64 encoded signature of the content + * @throws CryptographyException if an error occurs in the cryptography process + */ + public static String temporarySignature(String content, String privateKey) throws CryptographyException + { + return signContent(String.format("%s|%d", content, (System.currentTimeMillis() / 1000 / TIME_BLOCK)), privateKey); + } + + /** + * Generates a temporary signature for the given content using the provided private key. + * + * @param content the content to be signed + * @param privateKey the private key used for signing + * @return the base64 encoded signature of the content + * @throws CryptographyException if an error occurs in the cryptography process + */ + public static String temporarySignature(String content, PrivateKey privateKey) throws CryptographyException + { + return signContent(String.format("%s|%d", content, (System.currentTimeMillis() / 1000 / TIME_BLOCK)), privateKey); + } + + /** + * Verifies the temporary signature of the given content using the specified public key and number of frames. + * + * @param content The content to verify the signature for. Must not be null. + * @param signature The signature to verify. Must not be null. + * @param publicKey The public key used for verification. Must not be null. + * @param frames The number of frames to use for verification. Must be a positive integer. + * @return true if the signature is valid, false otherwise. + * @throws CryptographyException if an error occurs during verification. + */ + public static boolean verifyTemporarySignature(String content, String signature, String publicKey, int frames) throws CryptographyException + { + return verifyTemporarySignature(content, signature, importPublicKey(publicKey), frames); + } + + /** + * Verifies the temporary signature of the given content using the specified public key and number of frames. + * + * @param content The content to verify the signature for. Must not be null. + * @param signature The signature to verify. Must not be null. + * @param publicKey The public key used for verification. Must not be null. + * @param frames The number of frames to use for verification. Must be a positive integer. + * @return true if the signature is valid, false otherwise. + * @throws CryptographyException if an error occurs during verification. + */ + public static boolean verifyTemporarySignature(String content, String signature, PublicKey publicKey, int frames) throws CryptographyException + { + if (frames <= 0) frames = 1; + long currentTime = System.currentTimeMillis() / 1000 / TIME_BLOCK; + + for (int i = 0; i < frames; i++) + { + if (verifyContent(String.format("%s|%d", content, currentTime - i), signature, publicKey)) + { + return true; + } + } + return false; + } + + /** + * Encrypts the given content using the provided public key. + * + * @param content the content to be encrypted + * @param publicKey the public key used for encryption + * @return the encrypted content as a String + * @throws CryptographyException if an error occurs during the encryption process + */ + public static String encrypt(String content, String publicKey) throws CryptographyException + { + return encrypt(content, importPublicKey(publicKey)); + } + + /** + * Encrypts the given content using the provided public key. + * + * @param content the content to be encrypted + * @param publicKey the public key used for encryption + * @return the encrypted content as a Base64 encoded string + * @throws CryptographyException if an error occurs during the encryption process + */ + public static String encrypt(String content, PublicKey publicKey) throws CryptographyException + { + try + { + Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMATION); + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + + byte[] data = content.getBytes(); + int inputLength = data.length; + List outputList = new ArrayList<>(); + + for (int i = 0; i < inputLength; i += MAX_ENCRYPT_BLOCK_SIZE) + { + int length = Math.min(inputLength - i, MAX_ENCRYPT_BLOCK_SIZE); + byte[] encryptedBlock = cipher.doFinal(data, i, length); + for (byte b : encryptedBlock) + { + outputList.add(b); + } + } + + byte[] output = new byte[outputList.size()]; + for (int i = 0; i < outputList.size(); i++) + { + output[i] = outputList.get(i); + } + + return Base64.getEncoder().encodeToString(output); + } + catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) + { + throw new CryptographyException("Failed to encrypt content", e); + } + } + + /** + * Decrypts the given content using the provided private key. + * + * @param content the encrypted content to be decrypted + * @param privateKey the private key used for decryption + * @return the decrypted content as a String + * @throws CryptographyException if an error occurs during the decryption process + */ + public static String decrypt(String content, String privateKey) throws CryptographyException + { + return decrypt(content, importPrivateKey(privateKey)); + } + + /** + * Decrypts the given content using the provided private key. + * + * @param content the encrypted content to be decrypted + * @param privateKey the private key used for decryption + * @return the decrypted content as a String + * @throws CryptographyException if an error occurs during the decryption process + */ + public static String decrypt(String content, PrivateKey privateKey) throws CryptographyException + { + try + { + Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMATION); + cipher.init(Cipher.DECRYPT_MODE, privateKey); + + byte[] data = Base64.getDecoder().decode(content); + int inputLength = data.length; + List outputList = new ArrayList<>(); + + for (int i = 0; i < inputLength; i += MAX_DECRYPT_BLOCK_SIZE) + { + int length = Math.min(inputLength - i, MAX_DECRYPT_BLOCK_SIZE); + byte[] decryptedBlock = cipher.doFinal(data, i, length); + for (byte b : decryptedBlock) + { + outputList.add(b); + } + } + + byte[] output = new byte[outputList.size()]; + for (int i = 0; i < outputList.size(); i++) + { + output[i] = outputList.get(i); + } + + return new String(output); + } + catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) + { + throw new CryptographyException("Failed to decrypt content", e); + } + } + + /** + * Exports the given private key as a Base64 encoded string. + * + * @param privateKey the private key to be exported + * @return the Base64 encoded string representation of the private key + */ + public static String exportPrivateKey(PrivateKey privateKey) + { + return Base64.getEncoder().encodeToString(privateKey.getEncoded()); + } + + /** + * Exports the given private key as a Base64 encoded string. + * + * @param keyPair the private key to be exported + * @return the Base64 encoded string representation of the private key + */ + public static String exportPrivateKey(KeyPair keyPair) + { + return exportPrivateKey(keyPair.getPrivate()); + } + + /** + * Exports the given public key as a Base64 encoded string. + * + * @param publicKey the public key to be exported + * @return a Base64 encoded string representation of the public key + */ + public static String exportPublicKey(PublicKey publicKey) + { + return Base64.getEncoder().encodeToString(publicKey.getEncoded()); + } + + /** + * Exports the given public key as a Base64 encoded string. + * + * @param keyPair the public key to be exported + * @return a Base64 encoded string representation of the public key + */ + public static String exportPublicKey(KeyPair keyPair) + { + return exportPublicKey(keyPair.getPublic()); + } + + /** + * Imports a public key from a Base64 encoded string representation. + * + * @param publicKey the Base64 encoded string representation of the public key + * @return the imported PublicKey object + * @throws CryptographyException if there is an error importing the public key + */ + public static PublicKey importPublicKey(String publicKey) throws CryptographyException + { + try + { + byte[] keyBytes = Base64.getDecoder().decode(publicKey); + X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); + return getKeyFactory().generatePublic(spec); + } + catch (InvalidKeySpecException | NoSuchAlgorithmException e) + { + throw new CryptographyException("Failed to import public key", e); + } + } + + /** + * Imports a private key from a Base64 encoded string representation. + * + * @param privateKey the Base64 encoded string representation of the private key + * @return the imported PrivateKey object + * @throws CryptographyException if there is an error importing the private key + */ + public static PrivateKey importPrivateKey(String privateKey) throws CryptographyException + { + try + { + byte[] keyBytes = Base64.getDecoder().decode(privateKey); + PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes); + return getKeyFactory().generatePrivate(spec); + } + catch (InvalidKeySpecException | NoSuchAlgorithmException | IllegalArgumentException e) + { + throw new CryptographyException("Failed to import private key", e); + } + } + +} diff --git a/src/main/java/net/nosial/socialclient/classes/Resolver.java b/src/main/java/net/nosial/socialclient/classes/Resolver.java new file mode 100644 index 0000000..601e9fa --- /dev/null +++ b/src/main/java/net/nosial/socialclient/classes/Resolver.java @@ -0,0 +1,95 @@ +package net.nosial.socialclient.classes; + +import net.nosial.socialclient.exceptions.ResolutionException; +import net.nosial.socialclient.objects.ResolvedServer; + +import javax.naming.NamingException; +import javax.naming.directory.*; +import java.util.Hashtable; + +// Improved Resolver class +public class Resolver +{ + /** + * 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 env = new Hashtable<>(); + 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.negative.ttl", "0"); + + try + { + DirContext dirContext = new InitialDirContext(env); + Attributes attributes = dirContext.getAttributes("dns:/" + domain, new String[]{"TXT"}); + Attribute attributeTXT = attributes.get("TXT"); + + if (attributeTXT == null) + { + throw new ResolutionException("Failed to resolve DNS TXT records for " + domain); + } + + String endpoint = null; + StringBuilder publicKeyBuilder = new StringBuilder(); + boolean publicKeyFound = false; + + for (int i = 0; i < attributeTXT.size(); i++) + { + String value = (String) attributeTXT.get(i); + + // Remove surrounding quotes if present + if (value.startsWith("\"") && value.endsWith("\"")) + { + value = value.substring(1, value.length() - 1); + } + + // Split the value into fragments to find the relevant keys + String[] fragments = value.split("\\s+"); + 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) + { + throw new ResolutionException("Failed to resolve RPC endpoint for " + domain); + } + + 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); + } + } +} \ No newline at end of file diff --git a/src/main/java/net/nosial/socialclient/classes/RpcClient.java b/src/main/java/net/nosial/socialclient/classes/RpcClient.java new file mode 100644 index 0000000..77b1596 --- /dev/null +++ b/src/main/java/net/nosial/socialclient/classes/RpcClient.java @@ -0,0 +1,361 @@ +package net.nosial.socialclient.classes; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.squareup.okhttp.*; +import net.nosial.socialclient.abstracts.RpcResult; +import net.nosial.socialclient.exceptions.CryptographyException; +import net.nosial.socialclient.exceptions.ResolutionException; +import net.nosial.socialclient.objects.ResolvedServer; +import net.nosial.socialclient.objects.RpcRequest; + +import java.io.IOException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class RpcClient +{ + private final static ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private final static String CLIENT_NAME = "SocialClient Java"; + private final static String CLIENT_VERSION = "1.0"; + private final static MediaType MEDIA_TYPE_JSON = MediaType.parse("application/json; charset=utf-8"); + + private final String domain; + private final String endpoint; + private final PublicKey serverPublicKey; + private final OkHttpClient httpClient; + + private String sessionUuid; + private PrivateKey privateKey; + + /** + * Initializes a new instance of the RpcClient class using the specified domain. + * This constructor resolves the domain to obtain the RPC endpoint and public key. + * + * @param domain The domain to be resolved for the RPC endpoint and public key. + * @throws ResolutionException If the domain cannot be resolved or if required information is missing. + * @throws CryptographyException If an error occurs while importing the public key. + */ + protected RpcClient(String domain) throws ResolutionException, CryptographyException + { + this.domain = domain; + this.httpClient = new OkHttpClient(); + + // Resolve the domain to get the endpoint and public key + ResolvedServer resolvedServer = Resolver.resolveDomain(domain); + + this.endpoint = resolvedServer.getEndpoint(); + this.serverPublicKey = Cryptography.importPublicKey(resolvedServer.getPublicKey()); + this.sessionUuid = null; + this.privateKey = null; + } + + /** + * Constructs an RpcClient instance with the specified endpoint and public key. + * + * @param endpoint The endpoint to which RPC requests will be sent. + * @param serverPublicKey The public key used for cryptographic operations with the RPC server. + */ + public RpcClient(String endpoint, PublicKey serverPublicKey) + { + this.domain = null; + this.endpoint = endpoint; + this.serverPublicKey = serverPublicKey; + this.httpClient = new OkHttpClient(); + this.sessionUuid = null; + this.privateKey = null; + } + + /** + * Retrieves the domain associated with this RpcClient instance. + * + * @return the domain as a string. + */ + public String getDomain() + { + return this.domain; + } + + /** + * Returns the RPC endpoint for the client. + * + * @return the endpoint as a String. + */ + public String getEndpoint() + { + return this.endpoint; + } + + /** + * Returns the public key associated with the RpcClient instance. + * + * @return the public key as a PublicKey object. + */ + public PublicKey getServerPublicKey() + { + return this.serverPublicKey; + } + + /** + * Retrieves the session UUID associated with the RpcClient instance. + * + * @return the session UUID as a string. + */ + public String getSessionUuid() + { + return this.sessionUuid; + } + + /** + * Sets the session UUID and imports the provided private key. + * + * @param sessionUuid the unique identifier for this session + * @param privateKey the private key to be imported + * @throws CryptographyException if an error occurs while importing the private key + */ + public void setSession(String sessionUuid, String privateKey) throws CryptographyException + { + this.sessionUuid = sessionUuid; + this.privateKey = Cryptography.importPrivateKey(privateKey); + } + + /** + * Sets the session details for the RPC client. + * + * @param sessionUuid the UUID of the session + * @param privateKey the private key associated with the session + */ + public void setSession(String sessionUuid, PrivateKey privateKey) + { + this.sessionUuid = sessionUuid; + this.privateKey = privateKey; + } + + /** + * Clears the current session by setting the sessionUuid and privateKey fields to null. + * This method effectively logs out the user from the current session. + */ + public void clearSession() + { + this.sessionUuid = null; + this.privateKey = null; + } + + /** + * Sends an RPC request with the provided JSON data and returns a list of RpcResult objects. + * + * @param jsonData the JSON-formatted string representing the RPC request data. + * @return a list of RpcResult objects representing the response(s) from the RPC server. + * @throws RuntimeException if there is an error during the request or response processing. + */ + @SuppressWarnings("unchecked") + public List sendRequest(String jsonData) + { + final Request request = new Request.Builder() + {{ + this.url(endpoint); + this.post(RequestBody.create(MEDIA_TYPE_JSON, jsonData)); + this.addHeader("Client-Name", CLIENT_NAME); + this.addHeader("Client-Version", CLIENT_VERSION); + + if(sessionUuid != null) + { + this.addHeader("Session-UUID", sessionUuid); + } + + if(privateKey != null) + { + try + { + this.addHeader("Signature", Cryptography.signContent(jsonData, privateKey)); + } + catch(CryptographyException e) + { + throw new RuntimeException(e); + } + } + }}.build(); + + try + { + final Response response = this.httpClient.newCall(request).execute(); + final String responseString = response.body().string(); + + if (!response.isSuccessful()) + { + if(!responseString.isEmpty()) + { + throw new RuntimeException(responseString); + } + + throw new RuntimeException("Failed to send request"); + } + + if(response.code() == 204) + { + // The response is empty + return new ArrayList<>(); + } + + Object decoded = decode(responseString); + + // Singular object response + if(decoded instanceof List) + { + List> responseList = (List>) decoded; + List results = new ArrayList<>(responseList.size()); + for(Map responseMap : responseList) + { + results.add(RpcResult.fromMap(responseMap)); + } + + return results; + } + + if(decoded instanceof Map) + { + // Singular object response + List results = new ArrayList<>(1); + results.add(RpcResult.fromMap((Map) decoded)); + return results; + } + + + throw new RuntimeException("Failed to decode response"); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + + /** + * Sends an RPC request to the configured endpoint and returns the first result. + * + * @param request the RPC request to be sent + * @return the first RPC result received from the response + */ + public RpcResult sendRequest(RpcRequest request) + { + return sendRequest(encode(request.toMap())).getFirst(); + } + + /** + * Sends a list of RPC requests to the server and returns their results as a list of RpcResult objects. + * + * @param requests the list of RPC requests to be sent + * @return a list of RpcResult objects containing the responses from the server + */ + public List sendRequests(List requests) + { + return sendRequest(encode(requests)); + } + + + /** + * Decodes a JSON string into a corresponding Java object. If the JSON string + * represents an object, it decodes it into a Map. If the JSON string represents + * an array, it decodes it into a List. + * + * @param json the JSON string to be decoded + * @return a decoded Java object which can be a Map or a List depending on the JSON structure + * @throws RuntimeException if the JSON string is neither an object nor an array, or if decoding fails + */ + private static Object decode(String json) + { + try + { + JsonNode jsonNode = OBJECT_MAPPER.readTree(json); + if (jsonNode.isObject()) + { + return decodeJson(OBJECT_MAPPER.readValue(json, new TypeReference<>() {})); + } + else if (jsonNode.isArray()) + { + return decodeList(OBJECT_MAPPER.readValue(json, new TypeReference<>() {})); + } + else + { + throw new RuntimeException("JSON is neither an object nor an array"); + } + } + catch (JsonProcessingException e) + { + throw new RuntimeException("Failed to decode input", e); + } + } + + /** + * Recursively decodes a JSON structure represented as a Map. If the value + * of an entry is another Map, it recursively decodes that Map. If the value + * is a List, it passes the List to the decodeList method. + * + * @param map the JSON structure to be decoded represented as a Map + * @return the decoded JSON structure as a Map + */ + @SuppressWarnings("unchecked") + private static Map decodeJson(Map map) + { + for (Map.Entry entry : map.entrySet()) + { + if (entry.getValue() instanceof Map) + { + entry.setValue(decodeJson((Map) entry.getValue())); + } + else if (entry.getValue() instanceof List) + { + entry.setValue(decodeList((List) entry.getValue())); + } + } + + return map; + } + + /** + * Recursively decodes a list of objects, transforming any nested maps or lists within it. + * + * @param list the list of objects to be decoded + * @return the decoded list of objects, with all nested maps and lists transformed + */ + @SuppressWarnings("unchecked") + private static List decodeList(List list) + { + for (int i = 0; i < list.size(); i++) + { + final Object item = list.get(i); + if (item instanceof Map) + { + list.set(i, decodeJson((Map) item)); + } + else if (item instanceof List) + { + list.set(i, decodeList((List) item)); + } + } + + return list; + } + + /** + * Encodes the given input object to its JSON string representation. + * + * @param input the object to encode + * @return the JSON string representation of the input object + */ + private static String encode(Object input) + { + try + { + return OBJECT_MAPPER.writeValueAsString(input); + } + catch(JsonProcessingException e) + { + throw new RuntimeException("Failed to encode input to JSON Data", e); + } + } +} diff --git a/src/main/java/net/nosial/socialclient/enums/StandardErrorCodes.java b/src/main/java/net/nosial/socialclient/enums/StandardErrorCodes.java new file mode 100644 index 0000000..08d552d --- /dev/null +++ b/src/main/java/net/nosial/socialclient/enums/StandardErrorCodes.java @@ -0,0 +1,41 @@ +package net.nosial.socialclient.enums; + +public enum StandardErrorCodes +{ + UNKNOWN(-1), + + RPC_METHOD_NOT_FOUND(-1000), + RPC_INVALID_ARGUMENTS(-1001), + + INTERNAL_SERVER_ERROR(-2000), + SERVER_UNAVAILABLE(-2001), + + INVALID_PUBLIC_KEY(-3000), + SESSION_NOT_FOUND(-3001), + UNSUPPORTED_AUTHENTICATION_TYPE(-3002); + + private final int code; + + StandardErrorCodes(int code) + { + this.code = code; + } + + public int getCode() + { + return this.code; + } + + public static StandardErrorCodes resolveCode(int input) + { + for (StandardErrorCodes errorCode : StandardErrorCodes.values()) + { + if (errorCode.getCode() == input) + { + return errorCode; + } + } + + return UNKNOWN; + } +} \ No newline at end of file diff --git a/src/main/java/net/nosial/socialclient/exceptions/CryptographyException.java b/src/main/java/net/nosial/socialclient/exceptions/CryptographyException.java new file mode 100644 index 0000000..c1b1b93 --- /dev/null +++ b/src/main/java/net/nosial/socialclient/exceptions/CryptographyException.java @@ -0,0 +1,14 @@ +package net.nosial.socialclient.exceptions; + +public class CryptographyException extends Exception +{ + public CryptographyException(String message) + { + super(message); + } + + public CryptographyException(String message, Throwable cause) + { + super(message, cause); + } +} diff --git a/src/main/java/net/nosial/socialclient/exceptions/ResolutionException.java b/src/main/java/net/nosial/socialclient/exceptions/ResolutionException.java new file mode 100644 index 0000000..f0572c0 --- /dev/null +++ b/src/main/java/net/nosial/socialclient/exceptions/ResolutionException.java @@ -0,0 +1,14 @@ +package net.nosial.socialclient.exceptions; + +public class ResolutionException extends Exception +{ + public ResolutionException(String message) + { + super(message); + } + + public ResolutionException(String message, Throwable cause) + { + super(message, cause); + } +} diff --git a/src/main/java/net/nosial/socialclient/exceptions/RpcException.java b/src/main/java/net/nosial/socialclient/exceptions/RpcException.java new file mode 100644 index 0000000..65a9cf5 --- /dev/null +++ b/src/main/java/net/nosial/socialclient/exceptions/RpcException.java @@ -0,0 +1,6 @@ +package net.nosial.socialclient.exceptions; + +public class RpcException extends Exception +{ + +} diff --git a/src/main/java/net/nosial/socialclient/objects/KeyPair.java b/src/main/java/net/nosial/socialclient/objects/KeyPair.java new file mode 100644 index 0000000..b64970a --- /dev/null +++ b/src/main/java/net/nosial/socialclient/objects/KeyPair.java @@ -0,0 +1,6 @@ +package net.nosial.socialclient.objects; + +public class KeyPair +{ + +} diff --git a/src/main/java/net/nosial/socialclient/objects/ResolvedServer.java b/src/main/java/net/nosial/socialclient/objects/ResolvedServer.java new file mode 100644 index 0000000..9fd4cfa --- /dev/null +++ b/src/main/java/net/nosial/socialclient/objects/ResolvedServer.java @@ -0,0 +1,23 @@ +package net.nosial.socialclient.objects; + +public class ResolvedServer +{ + private final String endpoint; + private final String publicKey; + + public ResolvedServer(String endpoint, String publicKey) + { + this.endpoint = endpoint; + this.publicKey = publicKey; + } + + public String getEndpoint() + { + return this.endpoint; + } + + public String getPublicKey() + { + return this.publicKey; + } +} diff --git a/src/main/java/net/nosial/socialclient/objects/RpcError.java b/src/main/java/net/nosial/socialclient/objects/RpcError.java new file mode 100644 index 0000000..9b78319 --- /dev/null +++ b/src/main/java/net/nosial/socialclient/objects/RpcError.java @@ -0,0 +1,79 @@ +package net.nosial.socialclient.objects; + +import net.nosial.socialclient.abstracts.RpcResult; +import net.nosial.socialclient.enums.StandardErrorCodes; + +import java.util.Map; + +public class RpcError extends RpcResult +{ + private final String id; + private final String error; + private final StandardErrorCodes code; + + /** + * Constructs an instance of RpcError using data from the given map. + * + * @param data a map containing the data for the error. Must include keys "id" with a String value, + * "error" with a String value, and "code" with an integer value which represents the error code. + */ + public RpcError(Map data) + { + super(false); + this.id = (String) data.get("id"); + this.error = (String) data.get("error"); + this.code = StandardErrorCodes.resolveCode((int)data.get("code")); + } + + /** + * Retrieves the ID of the RPC error. + * + * @return the ID of the RPC error. + */ + public String getId() + { + return this.id; + } + + /** + * Retrieves the error message associated with this RPC error. + * + * @return the error message as a String. + */ + public String getError() + { + return this.error; + } + + /** + * Retrieves the standard error code associated with this RPC error. + * + * @return the standard error code. + */ + public StandardErrorCodes getCode() + { + return this.code; + } + + /** + * Returns the current RpcError instance as the error response. + * + * @return the current RpcError instance. + */ + @Override + public RpcError getErrorResponse() + { + return this; + } + + /** + * Returns null as RpcError does not provide a successful response. + * + * @return null since RpcError instances do not represent successful responses. + */ + @Override + public RpcResponse getResponse() + { + return null; + } +} diff --git a/src/main/java/net/nosial/socialclient/objects/RpcRequest.java b/src/main/java/net/nosial/socialclient/objects/RpcRequest.java new file mode 100644 index 0000000..2a4cfab --- /dev/null +++ b/src/main/java/net/nosial/socialclient/objects/RpcRequest.java @@ -0,0 +1,134 @@ +package net.nosial.socialclient.objects; + +import java.util.HashMap; +import java.util.Map; + +public class RpcRequest +{ + private final String method; + private final String id; + private final Map params; + + /** + * Initializes a new RpcRequest object using the provided data map. + * + * @param data a map containing the initial values for the RpcRequest. + * The map should have the following expected keys: + * - "method": a String representing the RPC method name + * - "id": a String representing the unique request identifier + * - "params": a Map containing parameters for the method call + */ + @SuppressWarnings("unchecked") + public RpcRequest(Map data) + { + this.method = (String) data.get("method"); + this.id = (String) data.get("id"); + this.params = (Map) data.get("params"); + } + + /** + * Constructs an RpcRequest with the specified method name, request id, and parameters. + * + * @param method the RPC method to be called + * @param id the unique identifier for this request + * @param params the parameters for the RPC method + */ + public RpcRequest(String method, String id, Map params) + { + this.method = method; + this.id = id; + this.params = params; + } + + /** + * Constructs an RPC request with a specified method and parameters. + * The request ID will be set to null. + * + * @param method the name of the method being called + * @param params the parameters to be sent with the method call + */ + public RpcRequest(String method, Map params) + { + this.id = null; + this.method = method; + this.params = params; + } + + /** + * Constructs an RpcRequest with the specified method and id. + * + * @param method The method name for the RPC request. + * @param id The id of the RPC request. + */ + public RpcRequest(String method, String id) + { + this.method = method; + this.id = id; + this.params = null; + } + + /** + * Constructs an RpcRequest with a specified method and no parameters or ID. + * + * @param method the RPC method name for the request + */ + public RpcRequest(String method) + { + this.id = null; + this.method = method; + this.params = null; + } + + /** + * Retrieves the identifier of the RPC request. + * + * @return the identifier of the request. + */ + public String getId() + { + return this.id; + } + + /** + * Gets the method name associated with the RPC request. + * + * @return the method name as a String. + */ + public String getMethod() + { + return this.method; + } + + /** + * Retrieves the parameters map associated with the RPC request. + * + * @return a Map containing the parameters of the RPC request. + */ + public Map getParams() + { + return this.params; + } + + /** + * Converts the current RpcRequest object into a map representation. + * + * @return a map containing the method, id, and params of the RpcRequest, if present. + */ + public Map toMap() + { + HashMap map = new HashMap<>(); + map.put("method", this.method); + + if(this.id != null) + { + map.put("id", this.id); + } + + if(this.params != null) + { + map.put("params", this.params); + } + + return map; + } +} diff --git a/src/main/java/net/nosial/socialclient/objects/RpcResponse.java b/src/main/java/net/nosial/socialclient/objects/RpcResponse.java new file mode 100644 index 0000000..94f9b8f --- /dev/null +++ b/src/main/java/net/nosial/socialclient/objects/RpcResponse.java @@ -0,0 +1,68 @@ +package net.nosial.socialclient.objects; + +import net.nosial.socialclient.abstracts.RpcResult; + +import java.util.Map; + +public class RpcResponse extends RpcResult +{ + private final String id; + private final Object result; + + /** + * Constructs an instance of RpcResponse using data from the given map. + * + * @param data a map containing the data for the response. Must include an "id" key with a String value + * and optionally a "result" key with any Object value. + */ + public RpcResponse(Map data) + { + super(true); + this.id = (String) data.get("id"); + this.result = data.get("result"); + } + + /** + * Retrieves the ID of the RPC response. + * + * @return the ID of the RPC response. + */ + public String getId() + { + return this.id; + } + + /** + * Retrieves the result object from the RPC response. + * + * @return the result object, which can be of any type + */ + public Object getResult() + { + return this.result; + } + + /** + * Returns the error response for an RPC operation. For the + * RpcResponse class, an error response is always null as + * it's assumed to be successful with no errors. + * + * @return null as RpcResponse does not have an error response. + */ + @Override + public RpcError getErrorResponse() + { + return null; + } + + /** + * Retrieves the current RpcResponse instance. + * + * @return the current RpcResponse instance. + */ + @Override + public RpcResponse getResponse() + { + return this; + } +} diff --git a/src/test/java/net/nosial/socialclient/classes/CryptographyTest.java b/src/test/java/net/nosial/socialclient/classes/CryptographyTest.java new file mode 100644 index 0000000..8c740f6 --- /dev/null +++ b/src/test/java/net/nosial/socialclient/classes/CryptographyTest.java @@ -0,0 +1,262 @@ +package net.nosial.socialclient.classes; + +import net.nosial.socialclient.exceptions.CryptographyException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.security.KeyPair; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; + +class CryptographyTest +{ + /** + * Method under test: Cryptography::generateKeyPair + */ + @Test + public void testGenerateKeyPair_Success() throws CryptographyException { + // action + KeyPair keyPair = Cryptography.generateKeyPair(); + + // assert + Assertions.assertNotNull(keyPair, "KeyPair should not be null"); + + Assertions.assertNotNull(keyPair.getPrivate(), "PrivateKey should not be null"); + Assertions.assertTrue(keyPair.getPrivate().getAlgorithm().contains("RSA"), "PrivateKey algorithm should be RSA"); + + Assertions.assertNotNull(keyPair.getPublic(), "PublicKey should not be null"); + Assertions.assertTrue(keyPair.getPublic().getAlgorithm().contains("RSA"), "PublicKey algorithm should be RSA"); + } + + @Test + public void testSignContent_Success() throws CryptographyException { + // Arrange + String content = "ExampleContent"; + KeyPair keyPair = Cryptography.generateKeyPair(); + String privateKey = Cryptography.exportPrivateKey(keyPair); + + // Act + String signature = Cryptography.signContent(content, privateKey); + + // Assert + assertNotNull(signature, "Signature should not be null"); + assertTrue(Cryptography.verifyContent(content, signature, Cryptography.exportPublicKey(keyPair)), "Signed content should be verified successfully"); + } + + @Test + public void testSignContent_WithInvalidPrivateKey_ShouldThrowException() { + // Arrange + String content = "ExampleContent"; + String invalidPrivateKey = "InvalidPrivateKey"; + + // Assert + assertThrows(CryptographyException.class, () -> Cryptography.signContent(content, invalidPrivateKey), "SignContent should throw CryptographyException with invalid private key"); + } + + + @Test + public void testVerifyContent_Success() throws CryptographyException { + // Arrange + String content = "ExampleContent"; + KeyPair keyPair = Cryptography.generateKeyPair(); + String privateKey = Cryptography.exportPrivateKey(keyPair); + String signature = Cryptography.signContent(content, privateKey); + String publicKey = Cryptography.exportPublicKey(keyPair); + + // Act & assert + assertTrue(Cryptography.verifyContent(content, signature, publicKey), "Content should be verified successfully with valid inputs"); + } + + @Test + public void testVerifyContent_FailureWithInvalidSignature() throws CryptographyException { + // Arrange + String content = "ExampleContent"; + String invalidSignature = "InvalidSignature"; + KeyPair keyPair = Cryptography.generateKeyPair(); + String publicKey = Cryptography.exportPublicKey(keyPair); + + // Act & assert + assertThrows(CryptographyException.class, () -> Cryptography.verifyContent(content, invalidSignature, publicKey), "verifyContent should throw CryptographyException with invalid signature"); + } + + @Test + public void testVerifyContent_FailureWithInvalidPublicKey() throws CryptographyException { + // Arrange + String content = "ExampleContent"; + String invalidPublicKey = "InvalidPublicKey"; + KeyPair keyPair = Cryptography.generateKeyPair(); + String privateKey = Cryptography.exportPrivateKey(keyPair); + String signature = Cryptography.signContent(content, privateKey); + + // Act & assert + assertThrows(CryptographyException.class, () -> Cryptography.verifyContent(content, signature, invalidPublicKey), "verifyContent should throw CryptographyException with invalid public key"); + } + + @Test + public void testTemporarySignature_Success() throws CryptographyException { + // Arrange + String content = "ExampleContent"; + KeyPair keyPair = Cryptography.generateKeyPair(); + String privateKey = Cryptography.exportPrivateKey(keyPair); + + // Act + String signature = Cryptography.temporarySignature(content, privateKey); + + // Assert + Assertions.assertNotNull(signature, "Signature should not be null"); + Assertions.assertTrue(Cryptography.verifyTemporarySignature(content, signature, Cryptography.exportPublicKey(keyPair), 1), "Signed content should be verified successfully"); + } + + @Test + public void testEncrypt_FailureWithNullPublicKey() { + // Arrange + String content = "ExampleContent"; + String nullPublicKey = null; + + // Act & Assert + Assertions.assertThrows(NullPointerException.class, () -> Cryptography.encrypt(content, nullPublicKey), "encrypt should throw NullPointerException when public key is null"); + } + + @Test + public void testTemporarySignature_FailureWithInvalidPrivateKey() { + // Arrange + String content = "ExampleContent"; + String invalidPrivateKey = "InvalidPrivateKey"; + + // Assert + Assertions.assertThrows(CryptographyException.class, () -> Cryptography.temporarySignature(content, invalidPrivateKey), "temporarySignature should throw CryptographyException with invalid private key"); + } + + @Test + public void testTemporarySignature_FailureWithInvalidContent() throws CryptographyException { + // Arrange + String content = "ExampleContent"; + String invalidContent = "InvalidExampleContent"; + KeyPair keyPair = Cryptography.generateKeyPair(); + String privateKey = Cryptography.exportPrivateKey(keyPair); + + // Act + String signature = Cryptography.temporarySignature(content, privateKey); + + // Assert + Assertions.assertFalse(Cryptography.verifyTemporarySignature(invalidContent, signature, Cryptography.exportPublicKey(keyPair), 1), "Temporary signature should fail to verify with invalid content"); + } + + /** + * Method under test: Cryptography::encrypt + */ + @Test + public void testEncrypt_Success() throws CryptographyException { + // Arrange + String content = "ExampleContent"; + KeyPair keyPair = Cryptography.generateKeyPair(); + String publicKey = Cryptography.exportPublicKey(keyPair); + + // Act + String encryptedContent = Cryptography.encrypt(content, publicKey); + + // Assert + Assertions.assertNotNull(encryptedContent, "Encrypted content should not be null"); + } + + @Test + public void testEncrypt_FailureWithInvalidPublicKey() { + // Arrange + String content = "ExampleContent"; + String invalidPublicKey = "InvalidPublicKey"; + + // Act & Assert + Assertions.assertThrows(CryptographyException.class, () -> Cryptography.encrypt(content, invalidPublicKey), "encrypt should throw CryptographyException with invalid public key"); + + } + + /** + * Method under test: Cryptography::decrypt + */ + @Test + public void testDecrypt_Success() throws CryptographyException { + // Arrange + String originalContent = "ExampleContent"; + KeyPair keyPair = Cryptography.generateKeyPair(); + String publicKey = Cryptography.exportPublicKey(keyPair); + String privateKey = Cryptography.exportPrivateKey(keyPair); + String encryptedContent = Cryptography.encrypt(originalContent, publicKey); + + //Act + String decryptedContent = Cryptography.decrypt(encryptedContent, privateKey); + + // Assert + Assertions.assertNotNull(decryptedContent, "Decrypted content should not be null"); + Assertions.assertEquals(originalContent, decryptedContent, "Decrypted content should be equals to original content"); + } + + @Test + public void testDecrypt_FailureWithInvalidPrivateKey() { + // Arrange + String encryptedContent = "ExampleEncryptedContent"; + String invalidPrivateKey = "InvalidPrivateKey"; + + // Act & Assert + Assertions.assertThrows(CryptographyException.class, () -> Cryptography.decrypt(encryptedContent, invalidPrivateKey), "decrypt should throw CryptographyException with invalid private key"); + } + + @Test + public void testDecrypt_FailureWithNullPrivateKey() { + // Arrange + String encryptedContent = "ExampleEncryptedContent"; + String nullPrivateKey = null; + + // Act & Assert + Assertions.assertThrows(NullPointerException.class, () -> Cryptography.decrypt(encryptedContent, nullPrivateKey), "decrypt should throw NullPointerException when private key is null"); + } + + @Test + public void testEncrypt_NullContent_shouldThrowCryptographyException() throws CryptographyException { + // Arrange + String content = null; + KeyPair keyPair = Cryptography.generateKeyPair(); + String publicKey = Cryptography.exportPublicKey(keyPair); + + // Assert + assertThrows(NullPointerException.class, () -> Cryptography.encrypt(content, publicKey), "Encrypt should throw NullPointerException when content is null"); + } + + @Test + public void testEncrypt_EmptyContent_shouldRunSuccessfully() throws CryptographyException { + // Arrange + String content = ""; + KeyPair keyPair = Cryptography.generateKeyPair(); + String publicKey = Cryptography.exportPublicKey(keyPair); + + // Act + String encryptedContent = Cryptography.encrypt(content, publicKey); + + // Assert + assertNotNull(encryptedContent, "Encrypted content should not be null"); + } + + @Test + public void testEncrypt_LargeContent_shouldRunSuccessfully() throws CryptographyException { + // Arrange + String content = String.join("", Collections.nCopies(50000, "a")); + KeyPair keyPair = Cryptography.generateKeyPair(); + String publicKey = Cryptography.exportPublicKey(keyPair); + + // Act + String encryptedContent = Cryptography.encrypt(content, publicKey); + + // Assert + assertNotNull(encryptedContent, "Encrypted content should not be null"); + } + + @Test + public void testEncrypt_InvalidPublicKey_ShouldThrowException() { + // Arrange + String content = "ExampleContent"; + String invalidPublicKey = "InvalidPublicKey"; + + // Assert + assertThrows(CryptographyException.class, () -> Cryptography.encrypt(content, invalidPublicKey), "Encrypt should throw CryptographyException when public key is invalid"); + } +} diff --git a/src/test/java/net/nosial/socialclient/classes/RpcClientTest.java b/src/test/java/net/nosial/socialclient/classes/RpcClientTest.java new file mode 100644 index 0000000..c8c08c7 --- /dev/null +++ b/src/test/java/net/nosial/socialclient/classes/RpcClientTest.java @@ -0,0 +1,36 @@ +package net.nosial.socialclient.classes; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.squareup.okhttp.*; +import net.nosial.socialclient.abstracts.RpcResult; +import net.nosial.socialclient.exceptions.CryptographyException; +import net.nosial.socialclient.exceptions.ResolutionException; +import net.nosial.socialclient.objects.RpcRequest; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class RpcClientTest { + + @Test + public void testSendRequest() { + RpcClient rpcClient = null; + + try + { + rpcClient = new RpcClient("n64.cc"); + } + catch (Exception e) + { + fail(e.getMessage(), e); + } + + RpcResult result = rpcClient.sendRequest(new RpcRequest("ping", "abcd123")); + assertNotNull(result); + } +} \ No newline at end of file