Skip to content

Commit 54af471

Browse files
Java keystore and Base64 support (#552)
Add support for making a TlsContextOptions using a Java keystore, and bind Base64 encoding and decoding support.
1 parent dc195bb commit 54af471

4 files changed

Lines changed: 263 additions & 1 deletion

File tree

src/main/java/software/amazon/awssdk/crt/io/TlsContextOptions.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,51 @@ public static TlsContextOptions createWithMtlsWindowsCertStorePath(String certif
356356
return options;
357357
}
358358

359+
/**
360+
* Helper which creates mutual TLS (mTLS) options using a certificate and private key
361+
* stored in a Java keystore.
362+
* Will throw an exception if there is no certificate and key at the given certificate alias, or there is some other
363+
* error accessing or using the passed-in Java keystore.
364+
*
365+
* Note: function assumes the passed keystore has already been loaded from a file by calling "keystore.load()" or similar.
366+
*
367+
* @param keyStore The Java keystore to use. Assumed to be loaded with the desired certificate and key
368+
* @param certificateAlias The alias of the certificate and key to use.
369+
* @param certificatePassword The password of the certificate and key to use.
370+
* @throws CrtRuntimeException if the certificate alias does not exist or the certificate/key cannot be found in the certificate alias
371+
* @return A set of options for setting up an mTLS connection
372+
*/
373+
public static TlsContextOptions createWithMtlsJavaKeystore(
374+
java.security.KeyStore keyStore, String certificateAlias, String certificatePassword) {
375+
376+
TlsContextOptions options = new TlsContextOptions();
377+
String certificate;
378+
try {
379+
java.security.cert.Certificate certificateData = keyStore.getCertificate(certificateAlias);
380+
if (certificateData == null) {
381+
throw new CrtRuntimeException("Certificate at given certificate alias does not exist or does not contain a certificate");
382+
}
383+
String certificateString = new String(StringUtils.base64Encode(certificateData.getEncoded()));
384+
certificate = "-----BEGIN CERTIFICATE-----\n" + certificateString + "-----END CERTIFICATE-----\n";
385+
} catch (java.security.KeyStoreException | java.security.cert.CertificateEncodingException ex) {
386+
throw new RuntimeException("Failed to get certificate from Java keystore", ex);
387+
}
388+
String privateKey;
389+
try {
390+
java.security.Key keyData = keyStore.getKey(certificateAlias, certificatePassword.toCharArray());
391+
if (keyData == null) {
392+
throw new CrtRuntimeException("Private key at given certificate alias does not exist or does not identify a key-related entity");
393+
}
394+
String keyString = new String(StringUtils.base64Encode(keyData.getEncoded()));
395+
privateKey = "-----BEGIN RSA PRIVATE KEY-----\n" + keyString + "-----END RSA PRIVATE KEY-----\n";
396+
} catch (java.security.KeyStoreException | java.security.NoSuchAlgorithmException | java.security.UnrecoverableKeyException ex) {
397+
throw new RuntimeException("Failed to get private key from Java keystore", ex);
398+
}
399+
options.initMtls(certificate, privateKey);
400+
options.verifyPeer = true;
401+
return options;
402+
}
403+
359404
/*******************************************************************************
360405
* .with() methods
361406
******************************************************************************/

src/main/java/software/amazon/awssdk/crt/utils/StringUtils.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,25 @@ public static String join(CharSequence delimiter, Iterable<? extends CharSequenc
2323
}
2424
return sb.toString();
2525
}
26+
27+
/**
28+
* Encode a byte array into a Base64 byte array.
29+
* @param data The byte array to encode
30+
* @return The byte array encoded as Byte64
31+
*/
32+
public static byte[] base64Encode(byte[] data) {
33+
return stringUtilsBase64Encode(data);
34+
}
35+
36+
/**
37+
* Decode a Base64 byte array into a non-Base64 byte array.
38+
* @param data The byte array to decode.
39+
* @return Byte array decoded from Base64.
40+
*/
41+
public static byte[] base64Decode(byte[] data) {
42+
return stringUtilsBase64Decode(data);
43+
}
44+
45+
private static native byte[] stringUtilsBase64Encode(byte[] data_to_encode);
46+
private static native byte[] stringUtilsBase64Decode(byte[] data_to_decode);
2647
}

src/native/string_utils.c

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/**
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0.
4+
*/
5+
#include <jni.h>
6+
7+
#include <aws/common/encoding.h>
8+
#include <aws/common/string.h>
9+
10+
#include "crt.h"
11+
12+
JNIEXPORT
13+
jbyteArray JNICALL Java_software_amazon_awssdk_crt_utils_StringUtils_stringUtilsBase64Encode(
14+
JNIEnv *env,
15+
jclass jni_class,
16+
jbyteArray jni_data) {
17+
(void)jni_class;
18+
19+
struct aws_byte_cursor data_cursor;
20+
AWS_ZERO_STRUCT(data_cursor);
21+
struct aws_byte_buf formatted_data;
22+
AWS_ZERO_STRUCT(formatted_data);
23+
jbyteArray return_data = NULL;
24+
25+
data_cursor = aws_jni_byte_cursor_from_jbyteArray_acquire(env, jni_data);
26+
if (data_cursor.ptr == NULL) {
27+
return return_data;
28+
}
29+
30+
// Determine how much space we need
31+
size_t terminated_length = 0;
32+
if (aws_base64_compute_encoded_len(data_cursor.len, &terminated_length) != AWS_OP_SUCCESS) {
33+
aws_jni_throw_runtime_exception(env, "StringUtils: Could not determine length for base64 encode");
34+
goto clean_up;
35+
}
36+
37+
aws_byte_buf_init(&formatted_data, aws_jni_get_allocator(), terminated_length);
38+
int result = aws_base64_encode(&data_cursor, &formatted_data);
39+
if (result != AWS_OP_SUCCESS) {
40+
aws_jni_throw_runtime_exception(env, "StringUtils: Could not perform base64 encode");
41+
goto clean_up;
42+
}
43+
44+
struct aws_byte_cursor formatted_data_cursor = aws_byte_cursor_from_buf(&formatted_data);
45+
return_data = aws_jni_byte_array_from_cursor(env, &formatted_data_cursor);
46+
47+
clean_up:
48+
aws_jni_byte_cursor_from_jbyteArray_release(env, jni_data, data_cursor);
49+
aws_byte_buf_clean_up_secure(&formatted_data);
50+
return return_data;
51+
}
52+
53+
JNIEXPORT
54+
jbyteArray JNICALL Java_software_amazon_awssdk_crt_utils_StringUtils_stringUtilsBase64Decode(
55+
JNIEnv *env,
56+
jclass jni_class,
57+
jbyteArray jni_data) {
58+
(void)jni_class;
59+
60+
struct aws_byte_cursor data_cursor;
61+
AWS_ZERO_STRUCT(data_cursor);
62+
struct aws_byte_buf formatted_data;
63+
AWS_ZERO_STRUCT(formatted_data);
64+
jbyteArray return_data = NULL;
65+
66+
data_cursor = aws_jni_byte_cursor_from_jbyteArray_acquire(env, jni_data);
67+
if (data_cursor.ptr == NULL) {
68+
return NULL;
69+
}
70+
71+
// Determine how much space we need
72+
size_t terminated_length = 0;
73+
if (aws_base64_compute_decoded_len(&data_cursor, &terminated_length) != AWS_OP_SUCCESS) {
74+
aws_jni_throw_runtime_exception(env, "StringUtils: Could not determine length for base64 decode");
75+
goto clean_up;
76+
}
77+
78+
aws_byte_buf_init(&formatted_data, aws_jni_get_allocator(), terminated_length);
79+
int result = aws_base64_decode(&data_cursor, &formatted_data);
80+
if (result != AWS_OP_SUCCESS) {
81+
aws_jni_throw_runtime_exception(env, "StringUtils: Could not perform base64 decode");
82+
goto clean_up;
83+
}
84+
85+
struct aws_byte_cursor formatted_data_cursor = aws_byte_cursor_from_buf(&formatted_data);
86+
return_data = aws_jni_byte_array_from_cursor(env, &formatted_data_cursor);
87+
88+
clean_up:
89+
aws_jni_byte_cursor_from_jbyteArray_release(env, jni_data, data_cursor);
90+
aws_byte_buf_clean_up_secure(&formatted_data);
91+
return return_data;
92+
}

src/test/java/software/amazon/awssdk/crt/test/StringUtilsTest.java

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
package software.amazon.awssdk.crt.test;
22

33
import static org.junit.Assert.assertEquals;
4+
import static org.junit.Assert.assertThrows;
5+
import static org.junit.Assert.assertTrue;
46

57
import org.junit.Test;
8+
import org.junit.function.ThrowingRunnable;
69

710
import java.util.ArrayList;
811
import java.util.List;
912

1013
import software.amazon.awssdk.crt.utils.StringUtils;
1114

1215

13-
public class StringUtilsTest {
16+
public class StringUtilsTest extends CrtTestFixture {
1417

1518
@Test
1619
public void testJoin() {
@@ -20,4 +23,105 @@ public void testJoin() {
2023
alpns.add("two");
2124
assertEquals("one;two", StringUtils.join(";", alpns));
2225
}
26+
27+
@Test
28+
public void testBase64EncodeEmpty() {
29+
assertEquals("", new String(StringUtils.base64Encode("".getBytes())));
30+
}
31+
32+
@Test
33+
public void testBase64EncodeNull() {
34+
ThrowingRunnable test_runnable = new ThrowingRunnable() {
35+
public void run() {
36+
StringUtils.base64Encode(null);
37+
}
38+
};
39+
assertThrows(NullPointerException.class, test_runnable);
40+
}
41+
42+
@Test
43+
public void testBase64EncodeCaseFoobar() {
44+
assertEquals("Zm9vYmFy", new String(StringUtils.base64Encode("foobar".getBytes())));
45+
}
46+
47+
@Test
48+
public void testBase64EncodeExtremelyLargeString() {
49+
StringBuilder test_input = new StringBuilder();
50+
for (int i = 0; i < 50000; i++) {
51+
test_input.append('A');
52+
}
53+
byte[] output = StringUtils.base64Encode(test_input.toString().getBytes());
54+
assertTrue(output != null);
55+
}
56+
57+
@Test
58+
public void testBase64EncodeCaseAllValues() {
59+
byte[] data = new byte[255];
60+
for (int i = 0; i < 255; i++) {
61+
data[i] = (byte)(i);
62+
}
63+
64+
String expected = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERU";
65+
expected += "ZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouM";
66+
expected += "jY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0t";
67+
expected += "PU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+";
68+
69+
assertEquals(expected, new String(StringUtils.base64Encode(data)));
70+
}
71+
72+
@Test
73+
public void testBase64DecodeEmpty() {
74+
assertEquals("", new String(StringUtils.base64Decode("".getBytes())));
75+
}
76+
77+
@Test
78+
public void testBase64DecodeNull() {
79+
ThrowingRunnable test_runnable = new ThrowingRunnable() {
80+
public void run() {
81+
StringUtils.base64Decode(null);
82+
}
83+
};
84+
assertThrows(NullPointerException.class, test_runnable);
85+
}
86+
87+
@Test
88+
public void testBase64DecodeCaseFoobar() {
89+
assertEquals("foobar", new String(StringUtils.base64Decode("Zm9vYmFy".getBytes())));
90+
}
91+
92+
@Test
93+
public void testBase64DecodeExtremelyLargeString() {
94+
StringBuilder test_input = new StringBuilder();
95+
for (int i = 0; i < 50000; i++) {
96+
test_input.append('A');
97+
}
98+
byte[] output = StringUtils.base64Decode(test_input.toString().getBytes());
99+
assertTrue(output != null);
100+
}
101+
102+
@Test
103+
public void testBase64DecodeCaseAllValues() {
104+
byte[] data = new byte[255];
105+
for (int i = 0; i < 255; i++) {
106+
data[i] = (byte)(i);
107+
}
108+
109+
String input = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERU";
110+
input += "ZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouM";
111+
input += "jY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0t";
112+
input += "PU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+";
113+
114+
String expected = new String(data);
115+
116+
assertEquals(expected, new String(StringUtils.base64Decode(input.getBytes())));
117+
}
118+
119+
@Test
120+
public void testBase64CaseFoobarRoundTrop() {
121+
String data = "foobar";
122+
data = new String(StringUtils.base64Encode(data.getBytes()));
123+
assertEquals("Zm9vYmFy", data);
124+
data = new String(StringUtils.base64Decode(data.getBytes()));
125+
assertEquals("foobar", data);
126+
}
23127
}

0 commit comments

Comments
 (0)