Skip to content

Commit fc85f39

Browse files
committed
Add RSA Keyring.
1 parent be394d1 commit fc85f39

File tree

1 file changed

+168
-0
lines changed

1 file changed

+168
-0
lines changed
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
package software.amazon.encryption.s3.materials;
2+
3+
import java.nio.charset.StandardCharsets;
4+
import java.security.Key;
5+
import java.security.KeyPair;
6+
import java.security.SecureRandom;
7+
import java.security.spec.MGF1ParameterSpec;
8+
import java.util.ArrayList;
9+
import java.util.Arrays;
10+
import java.util.List;
11+
import javax.crypto.Cipher;
12+
import javax.crypto.SecretKey;
13+
import javax.crypto.spec.OAEPParameterSpec;
14+
import javax.crypto.spec.PSource.PSpecified;
15+
import javax.crypto.spec.SecretKeySpec;
16+
import software.amazon.encryption.s3.S3EncryptionClientException;
17+
18+
/**
19+
* RsaOaepKeyring will use an RSA public key to wrap the data key used to encrypt content.
20+
*/
21+
public class RsaOaepKeyring implements Keyring {
22+
23+
private static final String KEY_ALGORITHM = "RSA";
24+
private static final String KEY_PROVIDER_ID = "RSA-OAEP-SHA1";
25+
private static final String CIPHER_ALGORITHM = "RSA/ECB/OAEPPadding";
26+
private static final String DIGEST_NAME = "SHA-1";
27+
private static final String MGF_NAME = "MGF1";
28+
private static final MGF1ParameterSpec MGF_PARAMETER_SPEC = new MGF1ParameterSpec(DIGEST_NAME);
29+
private static final OAEPParameterSpec OAEP_PARAMETER_SPEC =
30+
new OAEPParameterSpec(DIGEST_NAME, MGF_NAME, MGF_PARAMETER_SPEC, PSpecified.DEFAULT);
31+
32+
private final KeyPair _wrappingKeyPair;
33+
private final SecureRandom _secureRandom;
34+
private final DataKeyGenerator _dataKeyGenerator;
35+
36+
private RsaOaepKeyring(Builder builder) {
37+
_wrappingKeyPair = builder._wrappingKeyPair;
38+
_secureRandom = builder._secureRandom;
39+
_dataKeyGenerator = builder._dataKeyGenerator;
40+
}
41+
42+
public static Builder builder() {
43+
return new Builder();
44+
}
45+
46+
@Override
47+
public EncryptionMaterials onEncrypt(EncryptionMaterials materials) {
48+
if (materials.plaintextDataKey() == null) {
49+
SecretKey dataKey = _dataKeyGenerator.generateDataKey(materials.algorithmSuite());
50+
materials = materials.toBuilder()
51+
.plaintextDataKey(dataKey.getEncoded())
52+
.build();
53+
}
54+
55+
try {
56+
final Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
57+
cipher.init(Cipher.WRAP_MODE, _wrappingKeyPair.getPublic(), OAEP_PARAMETER_SPEC, _secureRandom);
58+
59+
// Create a pseudo-data key with the content encryption appended to the data key
60+
byte[] dataKey = materials.plaintextDataKey();
61+
byte[] dataCipherName = materials.algorithmSuite().cipherName().getBytes(StandardCharsets.UTF_8);
62+
byte[] pseudoDataKey = new byte[1 + dataKey.length + dataCipherName.length];
63+
64+
pseudoDataKey[0] = (byte)dataKey.length;
65+
System.arraycopy(dataKey, 0, pseudoDataKey, 1, dataKey.length);
66+
System.arraycopy(dataCipherName, 0, pseudoDataKey, 1 + dataKey.length, dataCipherName.length);
67+
68+
byte[] ciphertext = cipher.wrap(new SecretKeySpec(pseudoDataKey, materials.algorithmSuite().dataKeyAlgorithm()));
69+
70+
EncryptedDataKey encryptedDataKey = EncryptedDataKey.builder()
71+
.keyProviderId(KEY_PROVIDER_ID)
72+
.ciphertext(ciphertext)
73+
.build();
74+
75+
List<EncryptedDataKey> encryptedDataKeys = new ArrayList<>(materials.encryptedDataKeys());
76+
encryptedDataKeys.add(encryptedDataKey);
77+
78+
return materials.toBuilder()
79+
.encryptedDataKeys(encryptedDataKeys)
80+
.build();
81+
} catch (Exception e) {
82+
throw new S3EncryptionClientException("Unable to " + KEY_PROVIDER_ID + " wrap", e);
83+
}
84+
}
85+
86+
@Override
87+
public DecryptionMaterials onDecrypt(final DecryptionMaterials materials, List<EncryptedDataKey> encryptedDataKeys) {
88+
if (materials.plaintextDataKey() != null) {
89+
return materials;
90+
}
91+
92+
for (EncryptedDataKey encryptedDataKey : encryptedDataKeys) {
93+
if (!encryptedDataKey.keyProviderId().equals(KEY_PROVIDER_ID)) {
94+
continue;
95+
}
96+
97+
try {
98+
final Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
99+
cipher.init(Cipher.UNWRAP_MODE, _wrappingKeyPair.getPrivate(), OAEP_PARAMETER_SPEC, _secureRandom);
100+
101+
String dataKeyAlgorithm = materials.algorithmSuite().dataKeyAlgorithm();
102+
Key pseudoDataKey = cipher.unwrap(encryptedDataKey.ciphertext(), dataKeyAlgorithm, Cipher.SECRET_KEY);
103+
104+
byte[] plaintext = parsePseudoDataKey(materials, pseudoDataKey.getEncoded());
105+
106+
return materials.toBuilder().plaintextDataKey(plaintext).build();
107+
} catch (Exception e) {
108+
throw new S3EncryptionClientException("Unable to " + KEY_PROVIDER_ID + " unwrap", e);
109+
}
110+
}
111+
112+
return materials;
113+
}
114+
115+
private byte[] parsePseudoDataKey(DecryptionMaterials materials, byte[] pseudoDataKey) {
116+
int dataKeyLengthBytes = pseudoDataKey[0];
117+
if (!(dataKeyLengthBytes == 16 || dataKeyLengthBytes == 24 || dataKeyLengthBytes == 32)) {
118+
throw new S3EncryptionClientException("Invalid key length (" + dataKeyLengthBytes + ") in encrypted data key");
119+
}
120+
121+
int dataCipherNameLength = pseudoDataKey.length - dataKeyLengthBytes - 1;
122+
if (dataCipherNameLength <= 0) {
123+
throw new S3EncryptionClientException("Invalid data cipher name length (" + dataCipherNameLength + ") in encrypted data key");
124+
}
125+
126+
byte[] dataKey = new byte[dataKeyLengthBytes];
127+
byte[] dataCipherName = new byte[dataCipherNameLength];
128+
System.arraycopy(pseudoDataKey, 1, dataKey, 0, dataKeyLengthBytes);
129+
System.arraycopy(pseudoDataKey, 1 + dataKeyLengthBytes, dataCipherName, 0, dataCipherNameLength);
130+
131+
byte[] expectedDataCipherName = materials.algorithmSuite().cipherName().getBytes(StandardCharsets.UTF_8);
132+
if (!Arrays.equals(expectedDataCipherName, dataCipherName)) {
133+
throw new S3EncryptionClientException("The data cipher does not match the data cipher used for encryption. The object may be altered or corrupted");
134+
}
135+
136+
return dataKey;
137+
}
138+
139+
public static class Builder {
140+
private KeyPair _wrappingKeyPair;
141+
private SecureRandom _secureRandom = new SecureRandom();
142+
private DataKeyGenerator _dataKeyGenerator = new DefaultDataKeyGenerator();
143+
144+
private Builder() {}
145+
146+
public Builder wrappingKeyPair(KeyPair wrappingKeyPair) {
147+
if (!wrappingKeyPair.getPublic().getAlgorithm().equals(KEY_ALGORITHM)) {
148+
throw new S3EncryptionClientException("Invalid algorithm '" + wrappingKeyPair.getPublic().getAlgorithm() + "', expecting " + KEY_ALGORITHM);
149+
}
150+
_wrappingKeyPair = wrappingKeyPair;
151+
return this;
152+
}
153+
154+
public Builder secureRandom(SecureRandom secureRandom) {
155+
_secureRandom = secureRandom;
156+
return this;
157+
}
158+
159+
public Builder dataKeyGenerator(DataKeyGenerator dataKeyGenerator) {
160+
_dataKeyGenerator = dataKeyGenerator;
161+
return this;
162+
}
163+
164+
public RsaOaepKeyring build() {
165+
return new RsaOaepKeyring(this);
166+
}
167+
}
168+
}

0 commit comments

Comments
 (0)