/
DefaultConfidentialStore.java
150 lines (134 loc) · 5.06 KB
/
DefaultConfidentialStore.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
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();
}