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=" } };