From 16968427b32d26b9081a309a356dd7e6ca7ab331 Mon Sep 17 00:00:00 2001 From: Braydon Date: Sat, 2 Dec 2023 04:08:46 -0500 Subject: [PATCH] oops x.x --- Example-Java/README.md | 3 + Example-Java/pom.xml | 79 ++++ .../me/braydon/example/LicenseClient.java | 337 ++++++++++++++++++ .../main/java/me/braydon/example/Main.java | 35 ++ 4 files changed, 454 insertions(+) create mode 100644 Example-Java/README.md create mode 100644 Example-Java/pom.xml create mode 100644 Example-Java/src/main/java/me/braydon/example/LicenseClient.java create mode 100644 Example-Java/src/main/java/me/braydon/example/Main.java diff --git a/Example-Java/README.md b/Example-Java/README.md new file mode 100644 index 0000000..21ae714 --- /dev/null +++ b/Example-Java/README.md @@ -0,0 +1,3 @@ +# Example + +This is the example of how to interact with the license server from within Java. \ No newline at end of file diff --git a/Example-Java/pom.xml b/Example-Java/pom.xml new file mode 100644 index 0000000..121dbef --- /dev/null +++ b/Example-Java/pom.xml @@ -0,0 +1,79 @@ + + + 4.0.0 + + me.braydon + Example + 1.0.0 + + + 17 + ${java.version} + ${java.version} + UTF-8 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + ${java.version} + ${java.version} + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.1.0 + + + package + + shade + + + + + + + + + + + org.projectlombok + lombok + 1.18.30 + provided + + + + + com.google.code.gson + gson + 2.10.1 + compile + + + + + com.github.oshi + oshi-core + 6.4.2 + compile + + + + + com.squareup.okhttp3 + okhttp + 4.11.0 + compile + + + \ No newline at end of file diff --git a/Example-Java/src/main/java/me/braydon/example/LicenseClient.java b/Example-Java/src/main/java/me/braydon/example/LicenseClient.java new file mode 100644 index 0000000..afddfb8 --- /dev/null +++ b/Example-Java/src/main/java/me/braydon/example/LicenseClient.java @@ -0,0 +1,337 @@ +/* + * Copyright (c) 2023 Braydon (Rainnny). All rights reserved. + * + * For inquiries, please contact braydonrainnny@gmail.com + */ +package me.braydon.example; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import lombok.*; +import okhttp3.*; +import oshi.SystemInfo; +import oshi.hardware.CentralProcessor; +import oshi.hardware.ComputerSystem; +import oshi.hardware.HardwareAbstractionLayer; +import oshi.software.os.OperatingSystem; + +import javax.crypto.Cipher; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.X509EncodedKeySpec; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Base64; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * An example of how to interact + * with the license server. This + * can be conveniently used in + * any project by simply copying + * the class into your project. + * + * @author Braydon + * @see License Server + */ +public final class LicenseClient { + private static final String ALGORITHM = "RSA"; // The crypto algorithm to use + + /** + * The endpoint to use for downloading the {@link PublicKey}. + */ + private static final String PUBLIC_KEY_ENDPOINT = "/crypto/pub"; + + /** + * The endpoint to check licenses at. + */ + private static final String CHECK_ENDPOINT = "/check"; + + /** + * The {@link Gson} instance to use. + */ + private static final Gson GSON = new GsonBuilder().serializeNulls().create(); + + /** + * The URL of the license server to make requests to. + */ + @NonNull private final String appUrl; + + /** + * The product to use for client. + */ + @NonNull private final String product; + + /** + * The {@link OkHttpClient} to use for requests. + */ + @NonNull private final OkHttpClient httpClient; + + /** + * The {@link PublicKey} to use for encryption. + */ + @NonNull private final PublicKey publicKey; + + public LicenseClient(@NonNull String appUrl, @NonNull String product, @NonNull File publicKeyFile) { + this.appUrl = appUrl; + this.product = product; + httpClient = new OkHttpClient(); // Create a new http client + publicKey = fetchPublicKey(publicKeyFile); // Fetch our public key + } + + /** + * Read the public key from the given bytes. + * + * @param bytes the bytes of the public key + * @return the public key + * @see PrivateKey for public key + */ + @SneakyThrows + private static PublicKey readPublicKey(byte[] bytes) { + return KeyFactory.getInstance(ALGORITHM).generatePublic(new X509EncodedKeySpec(bytes)); + } + + /** + * Check the license with the given + * key for the given product. + * + * @param key the key to check + * @return the license response + * @see LicenseResponse for response + */ + @NonNull + public LicenseResponse check(@NonNull String key) { + String hardwareId = getHardwareId(); // Get the hardware id of the machine + + // Build the json body + Map body = new HashMap<>(); + body.put("key", encrypt(key)); + body.put("product", product); + body.put("hwid", encrypt(hardwareId)); + String bodyJson = GSON.toJson(body); // The json body + + MediaType mediaType = MediaType.parse("application/json"); // Ensure the media type is json + RequestBody requestBody = RequestBody.create(mediaType, bodyJson); // Build the request body + Request request = new Request.Builder() + .url(appUrl + CHECK_ENDPOINT) + .post(requestBody) + .build(); // Build the POST request + + Response response = null; // The response of the request + int responseCode = -1; // The response code of the request + try { // Attempt to execute the request + response = httpClient.newCall(request).execute(); + responseCode = response.code(); + + // If the response is successful, we can parse the response + if (response.isSuccessful()) { + ResponseBody responseBody = response.body(); + assert responseBody != null; // We don't want the response body being null + + JsonObject json = GSON.fromJson(responseBody.string(), JsonObject.class); // Parse the json + JsonElement description = json.get("description"); + JsonElement ownerSnowflake = json.get("ownerSnowflake"); + JsonElement ownerName = json.get("ownerName"); + JsonElement plan = json.get("plan"); + JsonElement latestVersion = json.get("latestVersion"); + + // Parsing the expiration date if we have one + JsonElement expires = json.get("expires"); + Date expiresDate = null; + if (!expires.isJsonNull()) { + OffsetDateTime offsetDateTime = OffsetDateTime.parse(expires.getAsString(), DateTimeFormatter.ISO_OFFSET_DATE_TIME); + expiresDate = Date.from(offsetDateTime.toInstant()); + } + + // Return the license response + return new LicenseResponse(200, null, + description.isJsonNull() ? null : description.getAsString(), + ownerSnowflake.isJsonNull() ? -1 : ownerSnowflake.getAsLong(), + ownerName.isJsonNull() ? null : ownerName.getAsString(), + plan.getAsString(), + latestVersion.getAsString(), + expires.isJsonNull() ? null : expiresDate + ); + } else { + ResponseBody errorBody = response.body(); // Get the error body + if (errorBody != null) { // If we have an error body, we can parse it + String errorResponse = errorBody.string(); + JsonObject jsonError = GSON.fromJson(errorResponse, JsonObject.class); + JsonElement errorMessage = jsonError.get("error"); + if (!errorMessage.isJsonNull()) { // We have an error message, return it + return new LicenseResponse(responseCode, errorMessage.getAsString()); + } + } + } + } catch (IOException ex) { + ex.printStackTrace(); + } finally { + // Close the response if it's open + if (response != null) { + response.close(); + } + } + // Return an unknown error + return new LicenseResponse(responseCode, "An unknown error occurred"); + } + + /** + * Fetch the public key. + *

+ * If the public key is not already present, we + * fetch it from the server. Otherwise, the public + * key is loaded from the file. + *

+ * + * @param publicKeyFile the public key file + * @return the public key + * @see PublicKey for public key + */ + @SneakyThrows + private PublicKey fetchPublicKey(@NonNull File publicKeyFile) { + byte[] publicKey; + if (publicKeyFile.exists()) { // Public key exists, use it + publicKey = Files.readAllBytes(publicKeyFile.toPath()); + } else { + Request request = new Request.Builder() + .url(appUrl + PUBLIC_KEY_ENDPOINT) + .build(); // Build the GET request + @Cleanup Response response = httpClient.newCall(request).execute(); // Make the request + if (!response.isSuccessful()) { // Response wasn't successful + throw new IOException("Failed to download the public key, got response " + response.code()); + } + ResponseBody body = response.body(); // Get the response body + assert body != null; // We need a response body + publicKey = body.bytes(); // Read our public key + + // Write the response to the public key file + try (FileOutputStream outputStream = new FileOutputStream(publicKeyFile)) { + outputStream.write(publicKey); + } + } + return readPublicKey(publicKey); + } + + /** + * Get the unique hardware + * identifier of this machine. + * + * @return the hardware id + */ + @NonNull + private String getHardwareId() { + SystemInfo systemInfo = new SystemInfo(); + OperatingSystem operatingSystem = systemInfo.getOperatingSystem(); + HardwareAbstractionLayer hardwareAbstractionLayer = systemInfo.getHardware(); + CentralProcessor centralProcessor = hardwareAbstractionLayer.getProcessor(); + ComputerSystem computerSystem = hardwareAbstractionLayer.getComputerSystem(); + + // Retrieve necessary hardware information + String vendor = operatingSystem.getManufacturer(); + String processorSerialNumber = computerSystem.getSerialNumber(); + String uuid = computerSystem.getHardwareUUID(); + String processorIdentifier = centralProcessor.getProcessorIdentifier().getIdentifier(); + int processors = centralProcessor.getLogicalProcessorCount(); + + // Generate a unique hardware id using the retrieved information + return String.format("%08x", vendor.hashCode()) + "-" + + String.format("%08x", processorSerialNumber.hashCode()) + "-" + + String.format("%08x", uuid.hashCode()) + "-" + + String.format("%08x", processorIdentifier.hashCode()) + "-" + processors; + } + + /** + * Encrypt the given input. + * + * @param input the encrypted input + * @return the encrypted result + */ + @SneakyThrows @NonNull + private String encrypt(@NonNull String input) { + Cipher cipher = Cipher.getInstance(ALGORITHM); // Create our cipher + cipher.init(Cipher.ENCRYPT_MODE, publicKey); // Set our mode and public key + return Base64.getEncoder().encodeToString(cipher.doFinal(input.getBytes())); // Return our encrypted result + } + + /** + * The response of a license check. + * + * @see #check(String) + */ + @AllArgsConstructor @Getter @ToString + public static class LicenseResponse { + /** + * The status code of the response. + */ + private final long status; + + /** + * The error in the response, null if none. + */ + private String error; + + /** + * The description of the license, present if valid. + */ + private String description; + + /** + * The Discord snowflake of the license owner, present + * if valid and there is an owner. + */ + private long ownerSnowflake; + + /** + * The Discord name of the license owner, present + * if valid and there is an owner. + */ + private String ownerName; + + /** + * The plan for this license. + */ + @NonNull private String plan; + + /** + * The latest version of the product this license is for. + */ + @NonNull private String latestVersion; + + /** + * The optional expiration {@link Date} of the license. + */ + private Date expires; + + public LicenseResponse(long status, @NonNull String error) { + this.status = status; + this.error = error; + } + + /** + * Check if the license is valid. + * + * @return true if valid, otherwise false + */ + public boolean isValid() { + return status == 200; + } + + /** + * Check if the license is permanent. + * + * @return true if permanent, otherwise false + */ + public boolean isPermanent() { + return expires == null; + } + } +} \ No newline at end of file diff --git a/Example-Java/src/main/java/me/braydon/example/Main.java b/Example-Java/src/main/java/me/braydon/example/Main.java new file mode 100644 index 0000000..4f11dab --- /dev/null +++ b/Example-Java/src/main/java/me/braydon/example/Main.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 Braydon (Rainnny). All rights reserved. + * + * For inquiries, please contact braydonrainnny@gmail.com + */ +package me.braydon.example; + +import java.io.File; + +/** + * @author Braydon + */ +public final class Main { + public static void main(String[] args) { + LicenseClient client = new LicenseClient("http://localhost:7500", "Example", new File("public.key")); // Create the client + LicenseClient.LicenseResponse response = client.check("XXXX-XXXX-XXXX-XXXX"); // Check our license + if (!response.isValid()) { // License isn't valid + System.err.println("Invalid license: " + response.getError()); + return; + } + // License is valid + System.out.println("License is valid!"); + if (response.getOwnerName() != null) { + System.out.println("Welcome " + response.getOwnerName() + "! Your plan is " + response.getPlan() + " and the latest version is " + response.getLatestVersion()); + } + if (response.getDescription() != null) { + System.out.println("Description: " + response.getDescription()); // License description + } + if (response.isPermanent()) { // License is permanent + System.out.println("Your license is permanent"); + } else { // License has an expiration date + System.out.printf("Your license will expire at: %s%n", response.getExpires().toInstant()); + } + } +} \ No newline at end of file