Skip to content

Instantly share code, notes, and snippets.

@jvz
Created March 10, 2020 18:33
Show Gist options
  • Save jvz/dc62641b56728100123d8d0d8110945a to your computer and use it in GitHub Desktop.
Save jvz/dc62641b56728100123d8d0d8110945a to your computer and use it in GitHub Desktop.
Incomplete proof of concept for https://issues.jenkins-ci.org/browse/JENKINS-61406
package io.jenkins.plugins.keystore;
import hudson.init.InitMilestone;
import hudson.init.Initializer;
import jenkins.model.Jenkins;
import jenkins.security.ConfidentialKey;
import jenkins.security.ConfidentialStore;
import org.apache.commons.io.IOUtils;
import javax.annotation.CheckForNull;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.io.Console;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.PosixFilePermissions;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
public class PasswordProtectedConfidentialStore extends ConfidentialStore {
@Initializer(after = InitMilestone.PLUGINS_STARTED)
public static void init() {
Jenkins.get().lookup.set(ConfidentialStore.class, new PasswordProtectedConfidentialStore());
}
// TODO: ideally, this would use Argon2, bcrypt, or scrypt, and AES-256 (or ChaCha20)
private static final String ALGORITHM = "PBEWithHmacSHA512AndAES_256";
private static final int ITERATION_COUNT = 12288;
private static final int KEY_LENGTH = 256 * 8;
private final Path root;
private final SecureRandom random;
private final SecretKey masterKey;
public PasswordProtectedConfidentialStore() {
try {
root = Jenkins.get().root.toPath().resolve("secrets");
if (Files.notExists(root)) {
Files.createDirectory(root,
PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------")));
}
random = SecureRandom.getInstanceStrong();
masterKey = readKey();
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("No support for password-based encryption", e);
} catch (InvalidKeySpecException e) {
throw new IllegalArgumentException(e);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private SecretKey readKey() throws NoSuchAlgorithmException, InvalidKeySpecException, IOException {
Console console = System.console();
if (console == null) {
throw new IllegalStateException("No console available to unlock Jenkins");
}
char[] password;
byte[] salt;
Path saltFile = root.resolve("master.salt");
if (Files.exists(saltFile)) {
password = console.readPassword("Enter master key password to unlock Jenkins: ");
String saltString = Files.readAllLines(saltFile).get(0);
salt = Base64.getDecoder().decode(saltString);
} else {
do {
password = console.readPassword("Enter new master key password to encrypt secrets: ");
} while (!Arrays.equals(password, console.readPassword("Re-enter password: ")));
salt = new byte[128];
random.nextBytes(salt);
String saltString = Base64.getEncoder().encodeToString(salt);
Files.write(saltFile, Collections.singleton(saltString), StandardOpenOption.CREATE_NEW);
}
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(ALGORITHM);
// iterations should be at least 10000
// TODO: need a way to verify this was the correct password before we go silently decrypting and encrypting
// everything incorrectly
KeySpec spec = new PBEKeySpec(password, salt, ITERATION_COUNT, KEY_LENGTH);
return keyFactory.generateSecret(spec);
}
@Override
protected void store(ConfidentialKey key, byte[] payload) throws IOException {
Path keyFile = root.resolve(key.getId());
try {
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, masterKey);
try (CipherOutputStream out = new CipherOutputStream(Files.newOutputStream(keyFile), cipher)) {
out.write(payload);
}
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e) {
throw new IOException(e);
}
}
@CheckForNull
@Override
protected byte[] load(ConfidentialKey key) throws IOException {
Path keyFile = root.resolve(key.getId());
if (Files.notExists(keyFile)) {
return null;
}
try {
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, masterKey);
try (CipherInputStream in = new CipherInputStream(Files.newInputStream(keyFile), cipher)) {
return IOUtils.toByteArray(in);
}
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e) {
throw new IOException(e);
}
}
@Override
public byte[] randomBytes(int size) {
if (size > 100000) {
throw new IllegalArgumentException("Too much random: " + size);
}
byte[] buf = new byte[size];
random.nextBytes(buf);
return buf;
}
}
@jvz
Copy link
Author

jvz commented Mar 11, 2020

See https://github.com/jvz/keystore-module for updated version.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment