[Refactor] Use GPG instead of using legacy java keystores
This commit is contained in:
@@ -32,6 +32,10 @@ dependencies {
|
|||||||
implementation gradleApi()
|
implementation gradleApi()
|
||||||
|
|
||||||
shadeMe "org.apache.commons:commons-lang3:${commons_lang}"
|
shadeMe "org.apache.commons:commons-lang3:${commons_lang}"
|
||||||
|
shadeMe "org.bouncycastle:bcprov-jdk18on:${bouncy}"
|
||||||
|
shadeMe "org.bouncycastle:bcpg-jdk18on:${bouncy}"
|
||||||
|
shadeMe "commons-io:commons-io:${commons_io}"
|
||||||
|
shadeMe "commons-codec:commons-codec:${commons_codec}"
|
||||||
|
|
||||||
compileOnly "org.projectlombok:lombok:${lombok}"
|
compileOnly "org.projectlombok:lombok:${lombok}"
|
||||||
annotationProcessor "org.projectlombok:lombok:${lombok}"
|
annotationProcessor "org.projectlombok:lombok:${lombok}"
|
||||||
|
@@ -4,4 +4,7 @@ version_patch=0
|
|||||||
|
|
||||||
# Dependencies
|
# Dependencies
|
||||||
lombok=1.18.30
|
lombok=1.18.30
|
||||||
commons_lang=3.14.0
|
commons_lang=3.14.0
|
||||||
|
commons_io=2.16.0
|
||||||
|
commons_codec=1.16.1
|
||||||
|
bouncy=1.77
|
91
readme.md
91
readme.md
@@ -6,12 +6,19 @@ A Simple Gradle plugin to help you sign your jars.
|
|||||||
|
|
||||||
### Getting Started
|
### Getting Started
|
||||||
|
|
||||||
To get started, you will need keystore. If you already have this, you can skip this part.
|
To get started, you will a GPG key. If you already have this, you can skip it.
|
||||||
|
|
||||||
In a terminal, or in command line, run the following command:
|
In a terminal, or in command line, run the following commands:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
keytool -genkey -alias YOUR_ALIAS_HERE -keyalg RSA -keysize 2048 -keystore keystore.jks
|
# generate the keys
|
||||||
|
gpg --gen-key
|
||||||
|
|
||||||
|
#export the private key with the specified id to a file
|
||||||
|
gpg --output {private key file name and path} --armor --export-secret-keys {key-id}
|
||||||
|
|
||||||
|
#export the public key with the specified id to a file
|
||||||
|
gpg --output {public key file name and path} --armor --export {key-id}
|
||||||
```
|
```
|
||||||
|
|
||||||
Answer the required questions, and your file will be generated once completed.
|
Answer the required questions, and your file will be generated once completed.
|
||||||
@@ -53,39 +60,35 @@ Finally, add the following to `build.gradle` file:
|
|||||||
```groovy
|
```groovy
|
||||||
import dev.firstdark.keymaster.tasks.SignJarTask
|
import dev.firstdark.keymaster.tasks.SignJarTask
|
||||||
|
|
||||||
|
// This is optional. These values can be configured on the task
|
||||||
|
keymaster {
|
||||||
|
// GPG Password
|
||||||
|
gpgPassword = "123456"
|
||||||
|
// GPG Key file, or String.
|
||||||
|
gpgKey = System.getenv("GPG_KEY")
|
||||||
|
// Generate a .sig file for signed jars, to be used for verification
|
||||||
|
generateSignature = true
|
||||||
|
}
|
||||||
|
|
||||||
// Register a custom task to sign your jar
|
// Register a custom task to sign your jar
|
||||||
tasks.register('signJar', SignJarTask) {
|
tasks.register('signJar', SignJarTask) {
|
||||||
// Depend on the task used to build your project
|
// Depend on the task used to build your project
|
||||||
dependsOn jar
|
dependsOn jar
|
||||||
|
|
||||||
// The input artifact. This can be a Task, File or File Name
|
// The input artifact. This can be a Task, File or File Name
|
||||||
artifactInput = jar
|
artifactInput = jar
|
||||||
|
|
||||||
// Optional. Set the output name of the signed jar. This defaults to the artifactInput file name, and will overwrite it
|
// Optional. Set the output name of the signed jar. This defaults to the artifactInput file name, and will overwrite it
|
||||||
outputFileName = "testsign"
|
outputFileName = "testsign"
|
||||||
|
|
||||||
// The password of your key
|
// GPG Private key file or string. Not required when the extension is used
|
||||||
keyPass = "123456"
|
gpgKey = System.getenv("GPG_KEY")
|
||||||
|
|
||||||
// Your key alias
|
|
||||||
keyStoreAlias = "testalias"
|
|
||||||
|
|
||||||
// Your keystore password
|
|
||||||
keyStorePass = "123456"
|
|
||||||
|
|
||||||
// Your keystore location
|
|
||||||
keyStore = "/home/hypherionsa/dummystore.jks"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Example of signing another jar
|
// GPG Private Key password. Not required when extension is used
|
||||||
tasks.register('signDummyJar', SignJarTask) {
|
gpgPassword = "123456"
|
||||||
dependsOn createDummyJar
|
|
||||||
artifactInput = createDummyJar
|
|
||||||
|
|
||||||
keyPass = "123456"
|
// Should the task generate a .sig file. Defaults to true, and not required when extension is used
|
||||||
keyStoreAlias = "testalias"
|
generateSignature = false
|
||||||
keyStorePass = "123456"
|
|
||||||
keyStore = "/home/hypherionsa/dummystore.jks"
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -126,8 +129,18 @@ Finally, add the following to `build.gradle.kts` file:
|
|||||||
import dev.firstdark.keymaster.tasks.SignJarTask
|
import dev.firstdark.keymaster.tasks.SignJarTask
|
||||||
import org.gradle.kotlin.dsl.register
|
import org.gradle.kotlin.dsl.register
|
||||||
|
|
||||||
|
// This is optional. These values can be configured on the task
|
||||||
|
extensions.configure<KeymasterExtension>("keymaster") {
|
||||||
|
// GPG Password
|
||||||
|
gpgPassword = "123456"
|
||||||
|
// GPG Key file, or String.
|
||||||
|
gpgKey = System.getenv("GPG_KEY")
|
||||||
|
// Generate a .sig file for signed jars, to be used for verification
|
||||||
|
generateSignature = true
|
||||||
|
}
|
||||||
|
|
||||||
// Register a custom task to sign your jar
|
// Register a custom task to sign your jar
|
||||||
val signJar by tasks.register<SignJarTask>("signJar") {
|
tasks.register("signJar", SignJarTask::class) {
|
||||||
// Depend on the task used to build your project
|
// Depend on the task used to build your project
|
||||||
dependsOn(tasks.jar)
|
dependsOn(tasks.jar)
|
||||||
|
|
||||||
@@ -137,28 +150,14 @@ val signJar by tasks.register<SignJarTask>("signJar") {
|
|||||||
// Optional. Set the output name of the signed jar. This defaults to the artifactInput file name, and will overwrite it
|
// Optional. Set the output name of the signed jar. This defaults to the artifactInput file name, and will overwrite it
|
||||||
outputFileName = "testsign"
|
outputFileName = "testsign"
|
||||||
|
|
||||||
// The password of your key
|
// GPG Private key file or string. Not required when the extension is used
|
||||||
keyPass = "123456"
|
gpgKey = System.getenv("GPG_KEY")
|
||||||
|
|
||||||
// Your key alias
|
// GPG Private Key password. Not required when extension is used
|
||||||
keyStoreAlias = "testalias"
|
gpgPassword = "123456"
|
||||||
|
|
||||||
// Your keystore password
|
// Should the task generate a .sig file. Defaults to true, and not required when extension is used
|
||||||
keyStorePass = "123456"
|
generateSignature = false
|
||||||
|
|
||||||
// Your keystore location
|
|
||||||
keyStore = "/home/hypherionsa/dummystore.jks"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Example of signing another jar
|
|
||||||
val signDummyJar by tasks.register<SignJarTask>("signDummyJar") {
|
|
||||||
dependsOn(tasks.createDummyJar)
|
|
||||||
artifactInput = tasks.createDummyJar
|
|
||||||
|
|
||||||
keyPass = "123456"
|
|
||||||
keyStoreAlias = "testalias"
|
|
||||||
keyStorePass = "123456"
|
|
||||||
keyStore = "/home/hypherionsa/dummystore.jks"
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of KeyMaster, licensed under the MIT License (MIT).
|
||||||
|
*
|
||||||
|
* Copyright (c) 2024 HypherionSA and Contributors
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package dev.firstdark.keymaster.plugin;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.gradle.api.Project;
|
||||||
|
import org.gradle.api.provider.Property;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author HypherionSA
|
||||||
|
* Plugin Extension for sharing common configs between multiple tasks
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class KeyMasterGradleExtension {
|
||||||
|
|
||||||
|
// Default properties. These are overridden by the values specified on the task
|
||||||
|
private final Property<String> gpgKey;
|
||||||
|
private final Property<String> gpgPassword;
|
||||||
|
private final Property<Boolean> generateSignature;
|
||||||
|
private final Property<String> outputDirectory;
|
||||||
|
|
||||||
|
public KeyMasterGradleExtension(Project project) {
|
||||||
|
this.gpgKey = project.getObjects().property(String.class);
|
||||||
|
this.gpgPassword = project.getObjects().property(String.class);
|
||||||
|
this.generateSignature = project.getObjects().property(Boolean.class).convention(true);
|
||||||
|
this.outputDirectory = project.getObjects().property(String.class).convention(project.getBuildDir() + "/libs");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -1,3 +1,9 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of KeyMaster, licensed under the MIT License (MIT).
|
||||||
|
*
|
||||||
|
* Copyright (c) 2024 HypherionSA and Contributors
|
||||||
|
*
|
||||||
|
*/
|
||||||
package dev.firstdark.keymaster.plugin;
|
package dev.firstdark.keymaster.plugin;
|
||||||
|
|
||||||
import org.gradle.api.Plugin;
|
import org.gradle.api.Plugin;
|
||||||
@@ -6,12 +12,13 @@ import org.jetbrains.annotations.NotNull;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @author HypherionSA
|
* @author HypherionSA
|
||||||
* Main plugin class. Mostly a dummy for this plugin
|
* Main plugin class.
|
||||||
*/
|
*/
|
||||||
public class KeyMasterGradlePlugin implements Plugin<Project> {
|
public class KeyMasterGradlePlugin implements Plugin<Project> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void apply(@NotNull Project target) {
|
public void apply(@NotNull Project target) {
|
||||||
target.getLogger().info("KeyMaster Plugin is activated");
|
target.getLogger().info("KeyMaster Plugin is activated");
|
||||||
|
target.getExtensions().create("keymaster", KeyMasterGradleExtension.class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,21 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of KeyMaster, licensed under the MIT License (MIT).
|
||||||
|
*
|
||||||
|
* Copyright (c) 2024 HypherionSA and Contributors
|
||||||
|
*
|
||||||
|
*/
|
||||||
package dev.firstdark.keymaster.tasks;
|
package dev.firstdark.keymaster.tasks;
|
||||||
|
|
||||||
|
import dev.firstdark.keymaster.plugin.KeyMasterGradleExtension;
|
||||||
|
import dev.firstdark.keymaster.utils.PluginUtils;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
import org.gradle.api.Project;
|
import org.bouncycastle.openpgp.*;
|
||||||
import org.gradle.api.provider.Provider;
|
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder;
|
||||||
|
import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
|
||||||
|
import org.gradle.api.GradleException;
|
||||||
import org.gradle.api.tasks.Input;
|
import org.gradle.api.tasks.Input;
|
||||||
|
import org.gradle.api.tasks.Optional;
|
||||||
import org.gradle.api.tasks.OutputFile;
|
import org.gradle.api.tasks.OutputFile;
|
||||||
import org.gradle.api.tasks.TaskAction;
|
import org.gradle.api.tasks.TaskAction;
|
||||||
import org.gradle.api.tasks.bundling.AbstractArchiveTask;
|
|
||||||
import org.gradle.jvm.tasks.Jar;
|
import org.gradle.jvm.tasks.Jar;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.*;
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.StandardCopyOption;
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.util.HashMap;
|
import java.security.Security;
|
||||||
import java.util.Map;
|
|
||||||
|
import static dev.firstdark.keymaster.utils.PluginUtils.isNullOrBlank;
|
||||||
|
import static dev.firstdark.keymaster.utils.PluginUtils.resolveFile;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author HypherionSA
|
* @author HypherionSA
|
||||||
@@ -31,14 +43,20 @@ public class SignJarTask extends Jar {
|
|||||||
private String outputFileName = "signed.jar";
|
private String outputFileName = "signed.jar";
|
||||||
|
|
||||||
// KeyStore values
|
// KeyStore values
|
||||||
private String keyStorePass;
|
private String gpgKey;
|
||||||
private String keyStore;
|
private String gpgPassword;
|
||||||
private String keyStoreAlias;
|
private Boolean generateSignature = true;
|
||||||
private String keyPass;
|
|
||||||
|
|
||||||
// Set the output directory. Defaults to build/libs
|
// Set the output directory. Defaults to build/libs
|
||||||
private String outputDirectory = getProject().getBuildDir() + "/libs";
|
private String outputDirectory = getProject().getBuildDir() + "/libs";
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private final KeyMasterGradleExtension extension;
|
||||||
|
|
||||||
|
public SignJarTask() {
|
||||||
|
extension = getProject().getExtensions().findByType(KeyMasterGradleExtension.class);
|
||||||
|
}
|
||||||
|
|
||||||
@Input
|
@Input
|
||||||
public Object getArtifactInput() {
|
public Object getArtifactInput() {
|
||||||
return resolveFile(getProject(), this.artifactInput);
|
return resolveFile(getProject(), this.artifactInput);
|
||||||
@@ -50,28 +68,40 @@ public class SignJarTask extends Jar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Input
|
@Input
|
||||||
public String getKeyStorePass() {
|
@Optional
|
||||||
return this.keyStorePass;
|
@Nullable
|
||||||
|
public String getGpgKey() {
|
||||||
|
if (!isNullOrBlank(gpgKey))
|
||||||
|
return gpgKey;
|
||||||
|
|
||||||
|
if (extension != null && !isNullOrBlank(extension.getGpgKey().getOrNull()))
|
||||||
|
return extension.getGpgKey().get();
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Input
|
@Input
|
||||||
public String getKeyStore() {
|
@Optional
|
||||||
return this.keyStore;
|
@Nullable
|
||||||
}
|
public String getGpgPassword() {
|
||||||
|
if (!isNullOrBlank(gpgPassword))
|
||||||
|
return gpgPassword;
|
||||||
|
|
||||||
@Input
|
if (extension != null && !isNullOrBlank(extension.getGpgPassword().getOrNull()))
|
||||||
public String getKeyStoreAlias() {
|
return extension.getGpgPassword().get();
|
||||||
return this.keyStoreAlias;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Input
|
return null;
|
||||||
public String getKeyPass() {
|
|
||||||
return this.keyPass;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Input
|
@Input
|
||||||
public String getOutputDirectory() {
|
public String getOutputDirectory() {
|
||||||
return this.outputDirectory;
|
if (!isNullOrBlank(outputDirectory))
|
||||||
|
return outputDirectory;
|
||||||
|
|
||||||
|
if (extension != null && !isNullOrBlank(extension.getOutputDirectory().getOrNull()))
|
||||||
|
return extension.getOutputDirectory().get();
|
||||||
|
|
||||||
|
return outputDirectory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@OutputFile
|
@OutputFile
|
||||||
@@ -79,6 +109,14 @@ public class SignJarTask extends Jar {
|
|||||||
return new File(outputDirectory, outputFileName);
|
return new File(outputDirectory, outputFileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Input
|
||||||
|
public Boolean getGenerateSignature() {
|
||||||
|
if (extension != null && extension.getGenerateSignature().isPresent())
|
||||||
|
return extension.getGenerateSignature().get();
|
||||||
|
|
||||||
|
return generateSignature;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main Task Logic
|
* Main Task Logic
|
||||||
*/
|
*/
|
||||||
@@ -86,14 +124,14 @@ public class SignJarTask extends Jar {
|
|||||||
@TaskAction
|
@TaskAction
|
||||||
public void doTask() {
|
public void doTask() {
|
||||||
// Check that input is supplied
|
// Check that input is supplied
|
||||||
if (artifactInput == null) {
|
if (getArtifactInput() == null) {
|
||||||
getProject().getLogger().error("Input cannot be null!");
|
getProject().getLogger().error("Input cannot be null!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that all the required values are supplied
|
// Check that all the required values are supplied
|
||||||
if (isNullOrBlank(keyPass) || isNullOrBlank(keyStore) || isNullOrBlank(keyStoreAlias) || isNullOrBlank(keyStorePass)) {
|
if (isNullOrBlank(getGpgKey()) || isNullOrBlank(getGpgPassword())) {
|
||||||
getLogger().error("Please provide all required parameters: keyStore, keyStoreAlias, keyStorePass, keyPass");
|
getLogger().error("Please provide all required parameters: keyStore, keyPass");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,10 +139,11 @@ public class SignJarTask extends Jar {
|
|||||||
File tempDir = new File(getProject().getBuildDir(), "signing");
|
File tempDir = new File(getProject().getBuildDir(), "signing");
|
||||||
tempDir.mkdirs();
|
tempDir.mkdirs();
|
||||||
|
|
||||||
|
// Try to sign the jar
|
||||||
try {
|
try {
|
||||||
processArtifact(artifactInput, tempDir);
|
processArtifact(getArtifactInput(), tempDir);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
getLogger().error("Failed to sign artifact {}", artifactInput, e);
|
getLogger().error("Failed to sign artifact {}", getArtifactInput(), e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the temp working dir
|
// Remove the temp working dir
|
||||||
@@ -118,36 +157,35 @@ public class SignJarTask extends Jar {
|
|||||||
* @throws IOException This is mostly thrown when a file copy error occurs
|
* @throws IOException This is mostly thrown when a file copy error occurs
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||||
private void processArtifact(Object input, File tempDir) throws IOException {
|
private void processArtifact(Object input, File tempDir) throws IOException, PGPException {
|
||||||
// Set up the input file
|
// Set up the input file
|
||||||
File inputFile = resolveFile(getProject(), input);
|
File inputFile = resolveFile(getProject(), input);
|
||||||
|
|
||||||
// Check if the output file is specified. If not, default to the input file
|
// Check if the output file is specified. If not, default to the input file
|
||||||
outputFileName = outputFileName.equalsIgnoreCase("signed.jar") ? inputFile.getName() : outputFileName + ".jar";
|
outputFileName = getOutputFileName().equalsIgnoreCase("signed.jar") ? inputFile.getName() : getOutputFileName() + ".jar";
|
||||||
|
|
||||||
// Copy the original input file to the temporary processing folder
|
// Copy the original input file to the temporary processing folder
|
||||||
File tempInput = new File(tempDir, inputFile.getName());
|
File tempInput = new File(tempDir, inputFile.getName());
|
||||||
Files.copy(inputFile.toPath(), tempInput.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
Files.copy(inputFile.toPath(), tempInput.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
|
||||||
// Create a temporary output jar
|
// Create a temporary output jar
|
||||||
File tempOutput = new File(tempDir, outputFileName);
|
File tempOutput = new File(tempDir, getOutputFileName());
|
||||||
|
File sigTempFile = new File(tempDir, tempOutput.getName() + ".sig");
|
||||||
|
File outputSigFile = new File(getOutputFile().getParentFile(), getOutputFile().getName() + ".sig");
|
||||||
|
|
||||||
// Configure the jar signing
|
// Sign the damn thing
|
||||||
Map<String, Object> map = new HashMap<>();
|
signGPG(tempInput, sigTempFile, tempOutput);
|
||||||
map.put("alias", keyStoreAlias);
|
|
||||||
map.put("storePass", keyStorePass);
|
|
||||||
map.put("jar", tempInput.getAbsolutePath());
|
|
||||||
map.put("signedJar", tempOutput.getAbsolutePath());
|
|
||||||
map.put("keypass", keyPass);
|
|
||||||
map.put("keyStore", resolveFile(getProject(), keyStore).getAbsolutePath());
|
|
||||||
|
|
||||||
// SIGN IT
|
|
||||||
getProject().getAnt().invokeMethod("signjar", map);
|
|
||||||
|
|
||||||
// Copy the signed jar to the libs folder
|
// Copy the signed jar to the libs folder
|
||||||
Files.copy(tempOutput.toPath(), getOutputFile().toPath(), StandardCopyOption.REPLACE_EXISTING);
|
Files.copy(tempOutput.toPath(), getOutputFile().toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
|
||||||
getProject().getLogger().lifecycle("Signed " + getOutputFile().getName() + " successfully");
|
// Copy Signature File
|
||||||
|
if (getGenerateSignature()) {
|
||||||
|
Files.copy(sigTempFile.toPath(), outputSigFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
sigTempFile.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
getProject().getLogger().lifecycle("Signed {} successfully", getOutputFile().getName());
|
||||||
|
|
||||||
// Cleanup the temporary files
|
// Cleanup the temporary files
|
||||||
tempOutput.delete();
|
tempOutput.delete();
|
||||||
@@ -155,45 +193,58 @@ public class SignJarTask extends Jar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper method to check if a supplied string is null or empty
|
* Main Signing logic. This handles reading the GPG keys, and doing the actual signing
|
||||||
* @param s The string to test
|
* @param inputFile The jar to be signed
|
||||||
* @return True if null or empty
|
* @param signatureFile The GPG private key file or string
|
||||||
|
* @param signedOutputFile The output, signed jar file
|
||||||
|
* @throws IOException Thrown when a file error occurs
|
||||||
|
* @throws PGPException Thrown when a signature error occurs
|
||||||
*/
|
*/
|
||||||
private boolean isNullOrBlank(String s) {
|
private void signGPG(File inputFile, File signatureFile, File signedOutputFile) throws IOException, PGPException {
|
||||||
if (s == null)
|
// Load Bouncy Castle
|
||||||
return true;
|
BouncyCastleProvider provider = new BouncyCastleProvider();
|
||||||
|
Security.addProvider(provider);
|
||||||
|
|
||||||
return StringUtils.isBlank(s);
|
// Load the GPG private key
|
||||||
}
|
byte[] keyBytes = PluginUtils.resolvePrivateKey(getGpgKey());
|
||||||
|
if (keyBytes == null) {
|
||||||
/**
|
throw new GradleException("Could not read GPG private key. Signing will fail");
|
||||||
* Resolve an Object to a File
|
|
||||||
* @param project The project the file potentially belongs to
|
|
||||||
* @param obj The object to process
|
|
||||||
* @return A File object, ready to use
|
|
||||||
*/
|
|
||||||
private File resolveFile(Project project, Object obj) {
|
|
||||||
if (obj == null) {
|
|
||||||
throw new NullPointerException("Null Path");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (obj instanceof Provider) {
|
// Process the private key
|
||||||
Provider<?> p = (Provider<?>) obj;
|
try (ByteArrayInputStream keyInputStream = new ByteArrayInputStream(keyBytes);
|
||||||
obj = p.get();
|
FileOutputStream sigOutputStream = new FileOutputStream(signatureFile);
|
||||||
}
|
FileOutputStream signedOutputStream = new FileOutputStream(signedOutputFile)) {
|
||||||
|
|
||||||
if (obj instanceof File) {
|
PGPSecretKey secretKey = PluginUtils.readSecretKey(keyInputStream);
|
||||||
return (File) obj;
|
PGPPrivateKey privateKey = secretKey.extractPrivateKey(new JcePBESecretKeyDecryptorBuilder().setProvider(provider).build(getGpgPassword().toCharArray()));
|
||||||
}
|
|
||||||
|
|
||||||
if (obj instanceof AbstractArchiveTask) {
|
PGPSignatureGenerator signature = new PGPSignatureGenerator(
|
||||||
return ((AbstractArchiveTask)obj).getArchiveFile().get().getAsFile();
|
new JcaPGPContentSignerBuilder(secretKey.getPublicKey().getAlgorithm(), PGPUtil.SHA1)
|
||||||
}
|
.setProvider(provider));
|
||||||
|
signature.init(PGPSignature.BINARY_DOCUMENT, privateKey);
|
||||||
|
|
||||||
if (obj instanceof String) {
|
// Write signature to output .sig file
|
||||||
return new File(obj.toString());
|
if (getGenerateSignature()) {
|
||||||
}
|
try (FileInputStream inputStream = new FileInputStream(inputFile)) {
|
||||||
|
byte[] buffer = new byte[4096];
|
||||||
|
int bytesRead;
|
||||||
|
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||||
|
signature.update(buffer, 0, bytesRead);
|
||||||
|
}
|
||||||
|
|
||||||
return project.file(obj);
|
signature.generate().encode(sigOutputStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the signed content to the output signed jar file
|
||||||
|
try (FileInputStream inputStream = new FileInputStream(inputFile)) {
|
||||||
|
byte[] buffer = new byte[4096];
|
||||||
|
int bytesRead;
|
||||||
|
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||||
|
signedOutputStream.write(buffer, 0, bytesRead);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
135
src/main/java/dev/firstdark/keymaster/utils/PluginUtils.java
Normal file
135
src/main/java/dev/firstdark/keymaster/utils/PluginUtils.java
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of KeyMaster, licensed under the MIT License (MIT).
|
||||||
|
*
|
||||||
|
* Copyright (c) 2024 HypherionSA and Contributors
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package dev.firstdark.keymaster.utils;
|
||||||
|
|
||||||
|
import org.apache.commons.codec.binary.Base64;
|
||||||
|
import org.apache.commons.io.FileUtils;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.bouncycastle.openpgp.*;
|
||||||
|
import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
|
||||||
|
import org.gradle.api.GradleException;
|
||||||
|
import org.gradle.api.Project;
|
||||||
|
import org.gradle.api.provider.Provider;
|
||||||
|
import org.gradle.api.tasks.bundling.AbstractArchiveTask;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Iterator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author HypherionSA
|
||||||
|
* Helper Methods used in the plugin
|
||||||
|
*/
|
||||||
|
public class PluginUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to load PGP secrets from the user specified input
|
||||||
|
* @param input The InputStream of the file/string to process
|
||||||
|
* @return The signing key
|
||||||
|
* @throws IOException Thrown when a file error occurs
|
||||||
|
* @throws PGPException Thrown when a signature error occurs
|
||||||
|
*/
|
||||||
|
public static PGPSecretKey readSecretKey(InputStream input) throws IOException, PGPException {
|
||||||
|
PGPSecretKeyRingCollection pgpSec = new PGPSecretKeyRingCollection(
|
||||||
|
PGPUtil.getDecoderStream(input), new JcaKeyFingerprintCalculator());
|
||||||
|
|
||||||
|
Iterator<PGPSecretKeyRing> keyRingIter = pgpSec.getKeyRings();
|
||||||
|
while (keyRingIter.hasNext()) {
|
||||||
|
PGPSecretKeyRing keyRing = keyRingIter.next();
|
||||||
|
|
||||||
|
Iterator<PGPSecretKey> keyIter = keyRing.getSecretKeys();
|
||||||
|
while (keyIter.hasNext()) {
|
||||||
|
PGPSecretKey key = keyIter.next();
|
||||||
|
|
||||||
|
if (key.isSigningKey()) {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalArgumentException("Can't find signing key in key ring.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to read a GPG private key from either a file, or String
|
||||||
|
* @param input File or String to process
|
||||||
|
* @return The read key bytes, or null
|
||||||
|
*/
|
||||||
|
public static byte[] resolvePrivateKey(String input) {
|
||||||
|
File f = new File(input);
|
||||||
|
|
||||||
|
if (f.exists() && f.isFile()) {
|
||||||
|
try {
|
||||||
|
input = FileUtils.readFileToString(f, StandardCharsets.UTF_8);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new GradleException("Failed to read GPG Private key", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isNullOrBlank(input)) {
|
||||||
|
if (input.startsWith("-----")) {
|
||||||
|
String[] parts = input.split("\n");
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (String part : parts) {
|
||||||
|
if (!part.startsWith("-----")) {
|
||||||
|
sb.append(part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Base64.decodeBase64(sb.toString());
|
||||||
|
} else {
|
||||||
|
return input.getBytes(StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to check if a supplied string is null or empty
|
||||||
|
* @param s The string to test
|
||||||
|
* @return True if null or empty
|
||||||
|
*/
|
||||||
|
public static boolean isNullOrBlank(String s) {
|
||||||
|
if (s == null)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return StringUtils.isBlank(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve an Object to a File
|
||||||
|
* @param project The project the file potentially belongs to
|
||||||
|
* @param obj The object to process
|
||||||
|
* @return A File object, ready to use
|
||||||
|
*/
|
||||||
|
public static File resolveFile(Project project, Object obj) {
|
||||||
|
if (obj == null) {
|
||||||
|
throw new NullPointerException("Null Path");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj instanceof Provider) {
|
||||||
|
Provider<?> p = (Provider<?>) obj;
|
||||||
|
obj = p.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj instanceof File) {
|
||||||
|
return (File) obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj instanceof AbstractArchiveTask) {
|
||||||
|
return ((AbstractArchiveTask)obj).getArchiveFile().get().getAsFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj instanceof String) {
|
||||||
|
return new File(obj.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return project.file(obj);
|
||||||
|
}
|
||||||
|
}
|
@@ -16,6 +16,16 @@ dependencies {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is optional. These values can be configured on the task
|
||||||
|
keymaster {
|
||||||
|
// GPG Password
|
||||||
|
gpgPassword = "123456"
|
||||||
|
// GPG Key file, or String.
|
||||||
|
gpgKey = System.getenv("GPG_KEY")
|
||||||
|
// Generate a .sig file for signed jars, to be used for verification
|
||||||
|
generateSignature = true
|
||||||
|
}
|
||||||
|
|
||||||
tasks.register('createDummyJar', Jar) {
|
tasks.register('createDummyJar', Jar) {
|
||||||
// Configure the JAR task to have no files
|
// Configure the JAR task to have no files
|
||||||
from {}
|
from {}
|
||||||
@@ -33,26 +43,12 @@ tasks.register('signJar', SignJarTask) {
|
|||||||
// Optional. Set the output name of the signed jar. This defaults to the artifactInput file name, and will overwrite it
|
// Optional. Set the output name of the signed jar. This defaults to the artifactInput file name, and will overwrite it
|
||||||
outputFileName = "testsign"
|
outputFileName = "testsign"
|
||||||
|
|
||||||
// The password of your key
|
// GPG Private key file or string. Not required when the extension is used
|
||||||
keyPass = "123456"
|
gpgKey = System.getenv("GPG_KEY")
|
||||||
|
|
||||||
// Your key alias
|
// GPG Private Key password. Not required when extension is used
|
||||||
keyStoreAlias = "testalias"
|
gpgPassword = "123456"
|
||||||
|
|
||||||
// Your keystore password
|
// Should the task generate a .sig file. Defaults to true, and not required when extension is used
|
||||||
keyStorePass = "123456"
|
generateSignature = false
|
||||||
|
}
|
||||||
// Your keystore location
|
|
||||||
keyStore = "/home/hypherionsa/dummystore.jks"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Example of signing another jar
|
|
||||||
tasks.register('signDummyJar', SignJarTask) {
|
|
||||||
dependsOn createDummyJar
|
|
||||||
artifactInput = createDummyJar
|
|
||||||
|
|
||||||
keyPass = "123456"
|
|
||||||
keyStoreAlias = "testalias"
|
|
||||||
keyStorePass = "123456"
|
|
||||||
keyStore = "/home/hypherionsa/dummystore.jks"
|
|
||||||
}
|
|
Binary file not shown.
Reference in New Issue
Block a user