diff --git a/core/src/main/java/org/jclouds/crypto/CryptoStreams.java b/core/src/main/java/org/jclouds/crypto/CryptoStreams.java
index 40b234b80c..bd5dfce581 100644
--- a/core/src/main/java/org/jclouds/crypto/CryptoStreams.java
+++ b/core/src/main/java/org/jclouds/crypto/CryptoStreams.java
@@ -77,7 +77,54 @@ public class CryptoStreams {
public static String base64(byte[] in) {
return Base64.encodeBytes(in, Base64.DONT_BREAK_LINES);
}
+
+ /**
+ * encodes value substituting {@code '-' and '_'} for {@code '+' and '/'},
+ * and without adding trailing {@code '='} padding.
+ *
+ * @see url-safe
+ * encoding
+ */
+ public static String base64URLSafe(byte[] in) {
+ String provisional = base64(in);
+ int length = provisional.length();
+ if (length == 0)
+ return provisional;
+ // we know base64 is in 4 character chunks, so out of bounds risk here
+ else if (provisional.charAt((length - 2)) == '=')
+ length-=2;
+ else if (provisional.charAt((length - 1)) == '=')
+ length-=1;
+
+ char[] tmp = new char[length];
+ for (int i = 0; i < length; i++) {
+ char c = provisional.charAt(i);
+ switch (c) {
+ case '+':
+ tmp[i] = '-';
+ break;
+ case '/':
+ tmp[i] = '_';
+ break;
+ default:
+ tmp[i] = c;
+ break;
+ }
+ }
+ return new String(tmp);
+ }
+
+ /**
+ * decodes base 64 encoded string, regardless of whether padding {@code '='} padding is present.
+ *
+ * Note this seamlessly handles the URL-safe case where {@code '-' and '_'} are substituted for {@code '+' and '/'}.
+ *
+ * @see url-safe
+ * encoding
+ */
public static byte[] base64(String in) {
return Base64.decode(in);
}
diff --git a/core/src/main/java/org/jclouds/crypto/SshKeys.java b/core/src/main/java/org/jclouds/crypto/SshKeys.java
index 1813851e41..4d58c7086a 100644
--- a/core/src/main/java/org/jclouds/crypto/SshKeys.java
+++ b/core/src/main/java/org/jclouds/crypto/SshKeys.java
@@ -45,7 +45,6 @@ import java.security.spec.RSAPublicKeySpec;
import java.util.Map;
import org.bouncycastle.openssl.PEMWriter;
-import org.jclouds.encryption.internal.Base64;
import org.jclouds.io.InputSuppliers;
import org.jclouds.util.Strings2;
@@ -102,7 +101,7 @@ public class SshKeys {
Iterable parts = Splitter.on(' ').split(Strings2.toStringAndClose(stream));
checkArgument(Iterables.size(parts) >= 2 && "ssh-rsa".equals(Iterables.get(parts, 0)),
"bad format, should be: ssh-rsa AAAAB3...");
- stream = new ByteArrayInputStream(Base64.decode(Iterables.get(parts, 1)));
+ stream = new ByteArrayInputStream(CryptoStreams.base64(Iterables.get(parts, 1)));
String marker = new String(readLengthFirst(stream));
checkArgument("ssh-rsa".equals(marker), "looking for marker ssh-rsa but got %s", marker);
BigInteger publicExponent = new BigInteger(readLengthFirst(stream));
diff --git a/core/src/main/java/org/jclouds/encryption/internal/Base64.java b/core/src/main/java/org/jclouds/encryption/internal/Base64.java
index b04effe796..6f5866620c 100644
--- a/core/src/main/java/org/jclouds/encryption/internal/Base64.java
+++ b/core/src/main/java/org/jclouds/encryption/internal/Base64.java
@@ -152,6 +152,12 @@ public class Base64
/**
* Translates a Base64 value to either its 6-bit reconstruction value
* or a negative number indicating some other meaning.
+ *
+ * Note
+ * {@code +} and {@code -} both decode to 62. {@code /} and {@code _} both decode to 63
+ *
+ * This accomodates URL-safe encoding
+ * @see url-safe base64
**/
private static final byte[] DECODABET =
{
@@ -164,7 +170,9 @@ public class Base64
-5, // Whitespace: Space
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 33 - 42
62, // Plus sign at decimal 43
- -9,-9,-9, // Decimal 44 - 46
+ -9, // Decimal 44
+ 62, // Hyphen at decimal 45
+ -9, // Decimal 45
63, // Slash at decimal 47
52,53,54,55,56,57,58,59,60,61, // Numbers zero through nine
-9,-9,-9, // Decimal 58 - 60
@@ -172,7 +180,9 @@ public class Base64
-9,-9,-9, // Decimal 62 - 64
0,1,2,3,4,5,6,7,8,9,10,11,12,13, // Letters 'A' through 'N'
14,15,16,17,18,19,20,21,22,23,24,25, // Letters 'O' through 'Z'
- -9,-9,-9,-9,-9,-9, // Decimal 91 - 96
+ -9,-9,-9,-9, // Decimal 91 - 94
+ 63, // Underscore at decimal 95
+ -9, // Decimal 96
26,27,28,29,30,31,32,33,34,35,36,37,38, // Letters 'a' through 'm'
39,40,41,42,43,44,45,46,47,48,49,50,51, // Letters 'n' through 'z'
-9,-9,-9,-9 // Decimal 123 - 126
@@ -199,7 +209,7 @@ public class Base64
-/* ******** E N C O D I N G ApiMetadata E T H O D S ******** */
+/* ******** E N C O D I N G M E T H O D S ******** */
/**
@@ -727,6 +737,16 @@ public class Base64
*/
public static byte[] decode( String s )
{
+ int modulo = s.length() % 4;
+ switch (modulo) {
+ case 2:
+ s += "==";
+ break;
+ case 3:
+ s += "=";
+ break;
+ }
+
byte[] bytes;
try
{
@@ -737,7 +757,7 @@ public class Base64
bytes = s.getBytes();
} // end catch
//
-
+
// Decode
bytes = decode( bytes, 0, bytes.length );
diff --git a/core/src/test/java/org/jclouds/crypto/CryptoStreamsTest.java b/core/src/test/java/org/jclouds/crypto/CryptoStreamsTest.java
index 2dcdac9010..d4914d06fc 100644
--- a/core/src/test/java/org/jclouds/crypto/CryptoStreamsTest.java
+++ b/core/src/test/java/org/jclouds/crypto/CryptoStreamsTest.java
@@ -31,7 +31,7 @@ import com.google.common.base.Charsets;
*
* @author Adrian Cole
*/
-@Test(groups = "unit", sequential = true)
+@Test(groups = "unit", singleThreaded = true)
public class CryptoStreamsTest {
@Test
@@ -43,11 +43,65 @@ public class CryptoStreamsTest {
@Test
public void testBase64Decode() throws IOException {
-
byte[] decoded = CryptoStreams.base64Decode(Payloads.newStringPayload("aGVsbG8gd29ybGQ="));
assertEquals(new String(decoded, Charsets.UTF_8), "hello world");
}
+ @Test
+ public void testBase64DecodeURLSafeJson() throws IOException {
+ byte[] decoded = CryptoStreams.base64("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9");
+ assertEquals(new String(decoded, Charsets.UTF_8), "{\"alg\":\"RS256\",\"typ\":\"JWT\"}");
+ }
+
+ @Test
+ public void testBase64DecodeURLSafeNoPadding() throws IOException {
+
+ byte[] decoded = CryptoStreams
+ .base64("eyJpc3MiOiI3NjEzMjY3OTgwNjktcjVtbGpsbG4xcmQ0bHJiaGc3NWVmZ2lncDM2bTc4ajVAZGV2ZWxvcGVyLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJzY29wZSI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL2F1dGgvcHJlZGljdGlvbiIsImF1ZCI6Imh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbS9vL29hdXRoMi90b2tlbiIsImV4cCI6MTMyODU1NDM4NSwiaWF0IjoxMzI4NTUwNzg1fQ");
+
+ assertEquals(new String(decoded, Charsets.UTF_8), "{"
+ + "\"iss\":\"761326798069-r5mljlln1rd4lrbhg75efgigp36m78j5@developer.gserviceaccount.com\","
+ + "\"scope\":\"https://www.googleapis.com/auth/prediction\","
+ + "\"aud\":\"https://accounts.google.com/o/oauth2/token\"," + "\"exp\":1328554385," + "\"iat\":1328550785"
+ + "}");
+ }
+
+ @Test
+ public void testBase64EncodeURLSafeNoSinglePad() {
+ assertEquals(CryptoStreams.base64("any carnal pleasu".getBytes(Charsets.UTF_8)), "YW55IGNhcm5hbCBwbGVhc3U=");
+ assertEquals(CryptoStreams.base64URLSafe("any carnal pleasu".getBytes(Charsets.UTF_8)), "YW55IGNhcm5hbCBwbGVhc3U");
+ }
+
+ @Test
+ public void testBase64EncodeURLSafeNoDoublePad() {
+ assertEquals(CryptoStreams.base64("any carnal pleas".getBytes(Charsets.UTF_8)), "YW55IGNhcm5hbCBwbGVhcw==");
+ assertEquals(CryptoStreams.base64URLSafe("any carnal pleas".getBytes(Charsets.UTF_8)), "YW55IGNhcm5hbCBwbGVhcw");
+ }
+
+ @Test
+ public void testBase64EncodeURLSafeHyphenNotPlus() {
+ assertEquals(CryptoStreams.base64("i?>".getBytes(Charsets.UTF_8)), "aT8+");
+ assertEquals(CryptoStreams.base64URLSafe("i?>".getBytes(Charsets.UTF_8)), "aT8-");
+ }
+
+ @Test
+ public void testBase64EncodeURLSafeUnderscoreNotSlash() {
+ assertEquals(CryptoStreams.base64("i??".getBytes(Charsets.UTF_8)), "aT8/");
+ assertEquals(CryptoStreams.base64URLSafe("i??".getBytes(Charsets.UTF_8)), "aT8_");
+ }
+
+ @Test
+ public void testBase64DecodeWithoutSinglePad() {
+ assertEquals(new String(CryptoStreams.base64("YW55IGNhcm5hbCBwbGVhc3U="), Charsets.UTF_8), "any carnal pleasu");
+ assertEquals(new String(CryptoStreams.base64("YW55IGNhcm5hbCBwbGVhc3U"), Charsets.UTF_8), "any carnal pleasu");
+ }
+
+ @Test
+ public void testBase64DecodeWithoutDoublePad() {
+ assertEquals(new String(CryptoStreams.base64("YW55IGNhcm5hbCBwbGVhcw=="), Charsets.UTF_8), "any carnal pleas");
+ assertEquals(new String(CryptoStreams.base64("YW55IGNhcm5hbCBwbGVhcw"), Charsets.UTF_8), "any carnal pleas");
+ }
+
@Test
public void testHexEncode() throws IOException {
diff --git a/core/src/test/java/org/jclouds/io/CryptoTest.java b/core/src/test/java/org/jclouds/io/CryptoTest.java
index df8a4504d7..03a8dcb4bd 100644
--- a/core/src/test/java/org/jclouds/io/CryptoTest.java
+++ b/core/src/test/java/org/jclouds/io/CryptoTest.java
@@ -29,10 +29,11 @@ import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
+import static javax.xml.bind.DatatypeConverter.*;
+
import org.jclouds.PerformanceTest;
import org.jclouds.crypto.Crypto;
import org.jclouds.crypto.CryptoStreams;
-import org.jclouds.encryption.internal.Base64;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
@@ -46,7 +47,7 @@ import com.google.inject.Injector;
* @author Adrian Cole
*/
// NOTE:without testName, this will not call @Before* and fail w/NPE during surefire
-@Test(groups = "performance", sequential = true, timeOut = 2 * 60 * 1000, testName = "CryptoTest")
+@Test(groups = "performance", singleThreaded = true, timeOut = 2 * 60 * 1000, testName = "CryptoTest")
public class CryptoTest extends PerformanceTest {
protected Crypto crypto;
@@ -58,16 +59,12 @@ public class CryptoTest extends PerformanceTest {
}
public static final Object[][] base64KeyMessageDigest = {
- { Base64.decode("CwsLCwsLCwsLCwsLCwsLCwsLCws="), "Hi There", "thcxhlUFcmTii8C2+zeMjvFGvgA=" },
- { Base64.decode("SmVmZQ=="), "what do ya want for nothing?", "7/zfauXrL6LSdBbV8YTfnCWafHk=" },
- { Base64.decode("DAwMDAwMDAwMDAwMDAwMDAwMDAw="), "Test With Truncation", "TBoDQktV4H/n8nvh1Yu5MkqaWgQ=" },
- {
- Base64
- .decode("qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqo="),
+ { parseBase64Binary("CwsLCwsLCwsLCwsLCwsLCwsLCws="), "Hi There", "thcxhlUFcmTii8C2+zeMjvFGvgA=" },
+ { parseBase64Binary("SmVmZQ=="), "what do ya want for nothing?", "7/zfauXrL6LSdBbV8YTfnCWafHk=" },
+ { parseBase64Binary("DAwMDAwMDAwMDAwMDAwMDAwMDAw="), "Test With Truncation", "TBoDQktV4H/n8nvh1Yu5MkqaWgQ=" },
+ { parseBase64Binary("qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqo="),
"Test Using Larger Than Block-Size Key - Hash Key First", "qkrl4VJy0A6VcFY3zoo7Ve1AIRI=" },
- {
- Base64
- .decode("qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqo="),
+ { parseBase64Binary("qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqo="),
"Test Using Larger Than Block-Size Key and Larger Than One Block-Size Data",
"6OmdD0UjfXhta7qnllx4CLv/GpE=" } };