Add cryptography

This commit is contained in:
Braydon 2023-12-02 03:26:57 -05:00
parent 9215ac87b0
commit 49b8fe39a5
22 changed files with 457 additions and 37 deletions

View File

@ -1,13 +1,15 @@
/*
* 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.AllArgsConstructor;
import lombok.Getter;
import lombok.NonNull;
import lombok.ToString;
import lombok.*;
import okhttp3.*;
import oshi.SystemInfo;
import oshi.hardware.CentralProcessor;
@ -15,9 +17,18 @@ 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;
@ -32,51 +43,93 @@ import java.util.Map;
* @author Braydon
* @see <a href="https://git.rainnny.club/Rainnny/LicenseServer">License Server</a>
*/
public final class LicenseExample {
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 = "http://localhost:7500/check";
private static final String CHECK_ENDPOINT = "/check";
/**
* The {@link Gson} instance to use.
*/
private static final Gson GSON = new GsonBuilder()
.serializeNulls()
.create();
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
* @param product the product the key belongs to
* @return the license response
* @see LicenseResponse for response
*/
@NonNull
public static LicenseResponse check(@NonNull String key, @NonNull String product) {
public LicenseResponse check(@NonNull String key) {
String hardwareId = getHardwareId(); // Get the hardware id of the machine
// Build the json body
Map<String, Object> body = new HashMap<>();
body.put("key", key);
body.put("key", encrypt(key));
body.put("product", product);
body.put("hwid", hardwareId);
body.put("hwid", encrypt(hardwareId));
String bodyJson = GSON.toJson(body); // The json body
OkHttpClient client = new OkHttpClient(); // Create a new http client
MediaType mediaType = MediaType.parse("application/json"); // Ensure the media type is json
RequestBody requestBody = RequestBody.create(bodyJson, mediaType); // Build the request body
RequestBody requestBody = RequestBody.create(mediaType, bodyJson); // Build the request body
Request request = new Request.Builder()
.url(CHECK_ENDPOINT)
.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 = client.newCall(request).execute();
response = httpClient.newCall(request).execute();
responseCode = response.code();
// If the response is successful, we can parse the response
@ -127,6 +180,43 @@ public final class LicenseExample {
return new LicenseResponse(responseCode, "An unknown error occurred");
}
/**
* Fetch the public key.
* <p>
* If the public key is not already present, we
* fetch it from the server. Otherwise, the public
* key is loaded from the file.
* </p>
*
* @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.
@ -134,7 +224,7 @@ public final class LicenseExample {
* @return the hardware id
*/
@NonNull
private static String getHardwareId() {
private String getHardwareId() {
SystemInfo systemInfo = new SystemInfo();
OperatingSystem operatingSystem = systemInfo.getOperatingSystem();
HardwareAbstractionLayer hardwareAbstractionLayer = systemInfo.getHardware();
@ -155,9 +245,25 @@ public final class LicenseExample {
+ String.format("%08x", processorIdentifier.hashCode()) + "-" + processors;
}
@AllArgsConstructor
@Getter
@ToString
/**
* 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.

View File

@ -1,11 +1,19 @@
/*
* 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) {
LicenseExample.LicenseResponse response = LicenseExample.check("XXXX-XXXX-XXXX-XXXX", "Example");
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;

View File

@ -1,3 +1,8 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license;
import com.google.gson.Gson;

View File

@ -0,0 +1,81 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license.common;
import lombok.NonNull;
import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import javax.crypto.Cipher;
import java.io.File;
import java.nio.file.Files;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
/**
* @author Braydon
*/
@UtilityClass
public final class CryptographyUtils {
private static final String ALGORITHM = "RSA"; // The algorithm to use
/**
* Generate a new key pair.
*
* @return the key pair
* @see KeyPair for key pair
*/
@NonNull @SneakyThrows
public static KeyPair generateKeyPair() {
KeyPairGenerator generator = KeyPairGenerator.getInstance(ALGORITHM); // Create a generator
generator.initialize(2048); // Set the key size
return generator.generateKeyPair(); // Return our generated key pair
}
/**
* Read the public key from the given file.
*
* @param keyFile the key file to read
* @return the public key
* @see PrivateKey for public key
*/
@SneakyThrows
public static PublicKey readPublicKey(@NonNull File keyFile) {
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(Files.readAllBytes(keyFile.toPath())); // Get the key spec
return KeyFactory.getInstance(ALGORITHM).generatePublic(keySpec); // Return the public key from the key spec
}
/**
* Read the private key from the given file.
*
* @param keyFile the key file to read
* @return the private key
* @see PrivateKey for private key
*/
@SneakyThrows
public static PrivateKey readPrivateKey(@NonNull File keyFile) {
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Files.readAllBytes(keyFile.toPath())); // Get the key spec
return KeyFactory.getInstance(ALGORITHM).generatePrivate(keySpec); // Return the private key from the key spec
}
/**
* Decrypt the given input with
* the provided private key.
*
* @param input the encrypted input
* @param privateKey the private key
* @return the decrypted result
* @see PrivateKey for private key
*/
@SneakyThrows @NonNull
public static String decryptMessage(@NonNull String input, @NonNull PrivateKey privateKey) {
Cipher cipher = Cipher.getInstance(ALGORITHM); // Create the cipher
cipher.init(Cipher.DECRYPT_MODE, privateKey); // Set our mode and private key
return new String(cipher.doFinal(Base64.getDecoder().decode(input))); // Return our decrypted result
}
}

View File

@ -1,3 +1,8 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license.common;
import jakarta.servlet.http.HttpServletRequest;

View File

@ -1,3 +1,8 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license.common;
import lombok.NonNull;

View File

@ -1,3 +1,8 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license.common;
import lombok.NonNull;

View File

@ -1,3 +1,8 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license.common;
import lombok.NonNull;

View File

@ -0,0 +1,59 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license.controller;
import lombok.NonNull;
import me.braydon.license.model.License;
import me.braydon.license.service.CryptographyService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.security.PublicKey;
/**
* @author Braydon
*/
@RestController
@RequestMapping(value = "/crypto", produces = MediaType.APPLICATION_JSON_VALUE)
public final class CryptographyController {
/**
* The {@link CryptographyService} to use.
*/
@NonNull private final CryptographyService service;
@Autowired
public CryptographyController(@NonNull CryptographyService service) {
this.service = service;
}
/**
* Downloads the public key file.
*
* @return the response entity
* @see PublicKey for public key
* @see License for license
* @see ResponseEntity for response entity
*/
@GetMapping("/pub")
@ResponseBody
public ResponseEntity<Resource> publicKey() {
byte[] publicKey = service.getKeyPair().getPublic().getEncoded(); // Get the public key
String fileName = "public.key"; // The name of the file to download
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"")
.contentLength(publicKey.length)
.body(new ByteArrayResource(publicKey));
}
}

View File

@ -1,12 +1,19 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license.controller;
import jakarta.servlet.http.HttpServletRequest;
import lombok.NonNull;
import me.braydon.license.common.CryptographyUtils;
import me.braydon.license.common.IPUtils;
import me.braydon.license.dto.LicenseCheckBodyDTO;
import me.braydon.license.dto.LicenseDTO;
import me.braydon.license.exception.APIException;
import me.braydon.license.model.License;
import me.braydon.license.service.CryptographyService;
import me.braydon.license.service.LicenseService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
@ -14,6 +21,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.security.PrivateKey;
import java.util.Map;
/**
@ -22,14 +30,20 @@ import java.util.Map;
@RestController
@RequestMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE)
public final class LicenseController {
/**
* The {@link CryptographyService} to use.
*/
@NonNull private final CryptographyService cryptographyService;
/**
* The {@link LicenseService} to use.
*/
@NonNull private final LicenseService service;
@NonNull private final LicenseService licenseService;
@Autowired
public LicenseController(@NonNull LicenseService service) {
this.service = service;
public LicenseController(@NonNull CryptographyService cryptographyService, @NonNull LicenseService licenseService) {
this.cryptographyService = cryptographyService;
this.licenseService = licenseService;
}
/**
@ -53,24 +67,34 @@ public final class LicenseController {
if (IPUtils.getIpType(ip) == -1) {
throw new APIException(HttpStatus.BAD_REQUEST, "Invalid IP address");
}
// Ensure the HWID is valid
String hwidString = body.getHwid();
String key;
String hwid;
try {
PrivateKey privateKey = cryptographyService.getKeyPair().getPrivate(); // Get our private key
key = CryptographyUtils.decryptMessage(body.getKey(), privateKey); // Decrypt our license key
hwid = CryptographyUtils.decryptMessage(body.getHwid(), privateKey); // Decrypt our hwid
} catch (IllegalArgumentException ex) {
throw new APIException(HttpStatus.BAD_REQUEST, "Signature Error");
}
// Validating that the UUID is in the correct format
boolean invalidHwid = true;
if (hwidString.contains("-")) {
int segments = hwidString.substring(0, hwidString.lastIndexOf("-")).split("-").length;
if (hwid.contains("-")) {
int segments = hwid.substring(0, hwid.lastIndexOf("-")).split("-").length;
if (segments == 4) {
invalidHwid = false;
}
}
if (invalidHwid) {
if (invalidHwid) { // Invalid HWID
throw new APIException(HttpStatus.BAD_REQUEST, "Invalid HWID");
}
// Check the license
License license = service.check(
body.getKey(),
License license = licenseService.check(
key,
body.getProduct(),
ip,
hwidString
hwid
);
// Return OK with the license DTO
return ResponseEntity.ok(new LicenseDTO(

View File

@ -1,3 +1,8 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license.dto;
import lombok.AllArgsConstructor;

View File

@ -1,3 +1,8 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license.dto;
import lombok.AllArgsConstructor;

View File

@ -1,3 +1,8 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license.exception;
import lombok.Getter;

View File

@ -1,3 +1,8 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license.exception;
import me.braydon.license.model.License;

View File

@ -1,3 +1,8 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license.exception;
import me.braydon.license.model.License;

View File

@ -1,3 +1,8 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license.exception;
import me.braydon.license.model.License;

View File

@ -1,3 +1,8 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license.exception;
import me.braydon.license.model.License;

View File

@ -1,3 +1,8 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license.model;
import lombok.Getter;

View File

@ -1,3 +1,8 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license.repository;
import lombok.NonNull;

View File

@ -0,0 +1,63 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license.service;
import lombok.Getter;
import lombok.NonNull;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import me.braydon.license.common.CryptographyUtils;
import org.springframework.stereotype.Service;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.KeyPair;
import java.util.Base64;
/**
* @author Braydon
*/
@Service
@Slf4j(topic = "Cryptography")
@Getter
public final class CryptographyService {
/**
* Our {@link KeyPair}.
*/
@NonNull private final KeyPair keyPair;
@SneakyThrows
public CryptographyService() {
File publicKeyFile = new File("public.key"); // The private key
File privateKeyFile = new File("private.key"); // The private key
if (!publicKeyFile.exists() || !privateKeyFile.exists()) { // Missing private key, generate new key pair.
keyPair = CryptographyUtils.generateKeyPair(); // Generate new key pair
writeKey(keyPair.getPublic().getEncoded(), publicKeyFile); // Write our public key
writeKey(keyPair.getPrivate().getEncoded(), privateKeyFile); // Write our private key
log.info("New key pair has been generated");
log.info(Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded()));
return;
}
// Load our private key from the file
keyPair = new KeyPair(CryptographyUtils.readPublicKey(publicKeyFile), CryptographyUtils.readPrivateKey(privateKeyFile));
log.info("Loaded private key from file " + privateKeyFile.getPath());
}
/**
* Write the given contents to the provided file.
*
* @param contents the content bytes to write
* @param file the file to write to
*/
private void writeKey(byte[] contents, @NonNull File file) {
try (FileOutputStream outputStream = new FileOutputStream(file)) {
outputStream.write(contents);
} catch (IOException ex) {
ex.printStackTrace();
}
}
}

View File

@ -1,3 +1,8 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license.service;
import com.google.common.cache.Cache;
@ -33,7 +38,6 @@ import net.dv8tion.jda.api.requests.GatewayIntent;
import org.mindrot.jbcrypt.BCrypt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.info.BuildProperties;
import org.springframework.stereotype.Service;
import java.awt.*;
@ -59,7 +63,7 @@ public final class DiscordService {
/**
* The version of this Springboot application.
*/
@NonNull private final String applicationVersion;
@NonNull private String applicationVersion = "n/a";
/**
* The salt to use for hashing license keys.
@ -140,9 +144,9 @@ public final class DiscordService {
.build();
@Autowired
public DiscordService(@NonNull LicenseRepository licenseRepository, @NonNull BuildProperties buildProperties) {
public DiscordService(@NonNull LicenseRepository licenseRepository/*, @NonNull BuildProperties buildProperties*/) {
this.licenseRepository = licenseRepository;
this.applicationVersion = buildProperties.getVersion();
// this.applicationVersion = buildProperties.getVersion();
}
@PostConstruct @SneakyThrows

View File

@ -1,3 +1,8 @@
/*
* Copyright (c) 2023 Braydon (Rainnny). All rights reserved.
*
* For inquiries, please contact braydonrainnny@gmail.com
*/
package me.braydon.license.service;
import jakarta.annotation.PostConstruct;