package jenkins.security;
import hudson.FilePath;
import hudson.Util;
import hudson.util.Secret;
import hudson.util.TextFile;
import jenkins.model.Jenkins;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.SecretKey;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import javax.crypto.BadPaddingException;
import org.apache.commons.io.IOUtils;
/**
* Default portable implementation of {@link ConfidentialStore} that uses
* a directory inside $JENKINS_HOME.
*
* The master key is also stored in this same directory.
*
* @author Kohsuke Kawaguchi
*/
// @MetaInfServices --- not annotated because this is the fallback implementation
public class DefaultConfidentialStore extends ConfidentialStore {
private final SecureRandom sr = new SecureRandom();
/**
* Directory that stores individual keys.
*/
private final File rootDir;
/**
* The master key.
*
* The sole purpose of the master key is to encrypt individual keys on the disk.
* Because leaking this master key compromises all the individual keys, we must not let
* this master key used for any other purpose, hence the protected access.
*/
private final SecretKey masterKey;
public DefaultConfidentialStore() throws IOException, InterruptedException {
this(new File(Jenkins.getInstance().getRootDir(),"secrets"));
}
public DefaultConfidentialStore(File rootDir) throws IOException, InterruptedException {
this.rootDir = rootDir;
if (rootDir.mkdirs()) {
// protect this directory. but don't change the permission of the existing directory
// in case the administrator changed this.
new FilePath(rootDir).chmod(0700);
}
TextFile masterSecret = new TextFile(new File(rootDir,"master.key"));
if (!masterSecret.exists()) {
// we are only going to use small number of bits (since export control limits AES key length)
// but let's generate a long enough key anyway
masterSecret.write(Util.toHexString(randomBytes(128)));
}
this.masterKey = Util.toAes128Key(masterSecret.readTrim());
}
/**
* Persists the payload of {@link ConfidentialKey} to the disk.
*/
@Override
protected void store(ConfidentialKey key, byte[] payload) throws IOException {
CipherOutputStream cos=null;
FileOutputStream fos=null;
try {
Cipher sym = Secret.getCipher("AES");
sym.init(Cipher.ENCRYPT_MODE, masterKey);
cos = new CipherOutputStream(fos=new FileOutputStream(getFileFor(key)), sym);
cos.write(payload);
cos.write(MAGIC);
} catch (GeneralSecurityException e) {
throw new IOException("Failed to persist the key: "+key.getId(),e);
} finally {
IOUtils.closeQuietly(cos);
IOUtils.closeQuietly(fos);
}
}
/**
* Reverse operation of {@link #store(ConfidentialKey, byte[])}
*
* @return
* null the data has not been previously persisted.
*/
@Override
protected byte[] load(ConfidentialKey key) throws IOException {
CipherInputStream cis=null;
FileInputStream fis=null;
try {
File f = getFileFor(key);
if (!f.exists()) return null;
Cipher sym = Secret.getCipher("AES");
sym.init(Cipher.DECRYPT_MODE, masterKey);
cis = new CipherInputStream(fis=new FileInputStream(f), sym);
byte[] bytes = IOUtils.toByteArray(cis);
return verifyMagic(bytes);
} catch (GeneralSecurityException e) {
throw new IOException("Failed to load the key: "+key.getId(),e);
} catch (IOException x) {
if (x.getCause() instanceof BadPaddingException) {
return null; // broken somehow
} else {
throw x;
}
} finally {
IOUtils.closeQuietly(cis);
IOUtils.closeQuietly(fis);
}
}
/**
* Verifies that the given byte[] has the MAGIC trailer, to verify the integrity of the decryption process.
*/
private byte[] verifyMagic(byte[] payload) {
int payloadLen = payload.length-MAGIC.length;
if (payloadLen<0) return null; // obviously broken
for (int i=0; i<MAGIC.length; i++) {
if (payload[payloadLen+i]!=MAGIC[i])
return null; // broken
}
byte[] truncated = new byte[payloadLen];
System.arraycopy(payload,0,truncated,0,truncated.length);
return truncated;
}
private File getFileFor(ConfidentialKey key) {
return new File(rootDir, key.getId());
}
public byte[] randomBytes(int size) {
byte[] random = new byte[size];
sr.nextBytes(random);
return random;
}
private static final byte[] MAGIC = "::::MAGIC::::".getBytes();
}