From e150f91b929b924f845832b0838103a438b3607f Mon Sep 17 00:00:00 2001 From: Charles Severance Date: Tue, 17 Oct 2023 10:59:21 -0400 Subject: [PATCH] SAK-49345 LTI switch from Base62 to a variant of Base64 (#11976) (cherry picked from commit 2ea87c8f3391dd83342539eb9f5eaf091993707f) --- .../org/sakaiproject/lti13/OIDCServlet.java | 4 +- .../sakaiproject/blti/tool/LTIAdminTool.java | 12 +- .../src/java/org/tsugi/util/Base62.java | 129 ---------------- .../tsugi/util/Base64DoubleUrlEncodeSafe.java | 55 +++++++ .../src/test/org/tsugi/util/Base62Test.java | 142 ------------------ .../util/Base64DoubleUrlEncodeSafeTest.java | 128 ++++++++++++++++ 6 files changed, 191 insertions(+), 279 deletions(-) delete mode 100644 basiclti/tsugi-util/src/java/org/tsugi/util/Base62.java create mode 100644 basiclti/tsugi-util/src/java/org/tsugi/util/Base64DoubleUrlEncodeSafe.java delete mode 100644 basiclti/tsugi-util/src/test/org/tsugi/util/Base62Test.java create mode 100644 basiclti/tsugi-util/src/test/org/tsugi/util/Base64DoubleUrlEncodeSafeTest.java diff --git a/basiclti/basiclti-oidc/src/java/org/sakaiproject/lti13/OIDCServlet.java b/basiclti/basiclti-oidc/src/java/org/sakaiproject/lti13/OIDCServlet.java index 00512eb7cd7a..8da97fb1e199 100644 --- a/basiclti/basiclti-oidc/src/java/org/sakaiproject/lti13/OIDCServlet.java +++ b/basiclti/basiclti-oidc/src/java/org/sakaiproject/lti13/OIDCServlet.java @@ -47,7 +47,7 @@ import org.tsugi.lti13.LTI13Util; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringEscapeUtils; -import org.tsugi.util.Base62; +import org.tsugi.util.Base64DoubleUrlEncodeSafe; import org.tsugi.http.HttpUtil; import org.sakaiproject.util.RequestFilter; @@ -253,7 +253,7 @@ private void fancyRedirect(HttpServletRequest request, HttpServletResponse respo * @param response */ private void handleResignContentItemResponse(HttpServletRequest request, HttpServletResponse response) throws IOException { - String forward = Base62.decode((String) request.getParameter("forward")); + String forward = Base64DoubleUrlEncodeSafe.decode((String) request.getParameter("forward")); forward = StringUtils.trimToNull(forward); Long toolKey = SakaiBLTIUtil.getLongKey((String) request.getParameter("tool_id")); diff --git a/basiclti/basiclti-tool/src/java/org/sakaiproject/blti/tool/LTIAdminTool.java b/basiclti/basiclti-tool/src/java/org/sakaiproject/blti/tool/LTIAdminTool.java index da7549535739..b6f0b97712cc 100644 --- a/basiclti/basiclti-tool/src/java/org/sakaiproject/blti/tool/LTIAdminTool.java +++ b/basiclti/basiclti-tool/src/java/org/sakaiproject/blti/tool/LTIAdminTool.java @@ -57,7 +57,7 @@ import org.json.simple.JSONObject; import org.json.simple.JSONArray; -import org.tsugi.util.Base62; +import org.tsugi.util.Base64DoubleUrlEncodeSafe; import static org.tsugi.basiclti.BasicLTIUtil.getObject; import static org.tsugi.basiclti.BasicLTIUtil.getString; @@ -1673,7 +1673,7 @@ public void doSingleContentItemResponse(RunData data, Context context) { } // Sanity check our (within Sakai) returnUrl - String returnUrl = Base62.decode(data.getParameters().getString("returnUrl")); + String returnUrl = Base64DoubleUrlEncodeSafe.decode(data.getParameters().getString("returnUrl")); if (returnUrl == null) { addAlert(state, rb.getString("error.contentitem.missing.returnurl")); switchPanel(state, errorPanel); @@ -2528,7 +2528,7 @@ public String buildContentConfigPanelContext(VelocityPortlet portlet, Context co + "?eventSubmit_doSingleContentItemResponse=Save" + "&" + FLOW_PARAMETER + "=" + flow + "&" + RequestFilter.ATTR_SESSION + "=" + URLEncoder.encode(sessionid + "." + suffix) - + "&returnUrl=" + Base62.encode(returnUrl) + + "&returnUrl=" + Base64DoubleUrlEncodeSafe.encode(returnUrl) + "&panel=PostContentItem" + "&tool_id=" + tool.get(LTIService.LTI_ID); @@ -2558,7 +2558,7 @@ public String buildContentConfigPanelContext(VelocityPortlet portlet, Context co // Run the contentreturn through the forward servlet contentReturn = serverConfigurationService.getServerUrl() + "/imsoidc/lti13/resigncontentitem?forward=" + - Base62.encode(contentReturn) + "&tool_id=" + tool.get(LTIService.LTI_ID); + Base64DoubleUrlEncodeSafe.encode(contentReturn) + "&tool_id=" + tool.get(LTIService.LTI_ID); contentLaunch = ContentItem.buildLaunch(contentLaunch, contentReturn, contentData); @@ -2796,7 +2796,7 @@ private String buildContentItemGenericMainPanelContext(VelocityPortlet portlet, + "/sakai.lti.admin.helper.helper" + "?panel=ContentConfig" + "&" + FLOW_PARAMETER + "=" + flow - + "&returnUrl=" + Base62.encode(returnUrl) + + "&returnUrl=" + Base64DoubleUrlEncodeSafe.encode(returnUrl) + "&tool_id=" + tool.get(LTIService.LTI_ID) + "&" + RequestFilter.ATTR_SESSION + "=" + URLEncoder.encode(sessionid + "." + suffix); context.put("forwardUrl", configUrl); @@ -2840,7 +2840,7 @@ private String buildContentItemGenericMainPanelContext(VelocityPortlet portlet, // Run the contentreturn through the forward servlet contentReturn = serverConfigurationService.getServerUrl() + "/imsoidc/lti13/resigncontentitem?forward=" + - Base62.encode(contentReturn) + "&tool_id=" + tool.get(LTIService.LTI_ID); + Base64DoubleUrlEncodeSafe.encode(contentReturn) + "&tool_id=" + tool.get(LTIService.LTI_ID); // This will forward to AccessServlet / BasicLTISecurityServiceImpl with a tool: url // AccessServlet will detect if this is a CI or DL and handle it accordingly using diff --git a/basiclti/tsugi-util/src/java/org/tsugi/util/Base62.java b/basiclti/tsugi-util/src/java/org/tsugi/util/Base62.java deleted file mode 100644 index eb14c3995c6d..000000000000 --- a/basiclti/tsugi-util/src/java/org/tsugi/util/Base62.java +++ /dev/null @@ -1,129 +0,0 @@ -/** - * This class provides utility methods for encoding and decoding data using the Base62 encoding scheme. - * Base62 is a binary-to-text encoding scheme that represents binary data using a set of 62 alphanumeric characters (A-Z, a-z, and 0-9). - * It is useful for converting binary data (e.g., byte arrays) into strings that are safe for use in URLs and other text-based contexts. - */ - -/* This code was written using the following ChatGPT prompts: - * - * Can you write a base62 encoder and decoder in java - * Could you make this encode and decode strings and not numbers - * Please complete the implementation details - * Please write the JavaDoc for this class. - * Please write a unit test for this class. - */ - -package org.tsugi.util; - -import java.util.HashMap; -import java.util.Map; - -public class Base62 { - - /** - * The characters used for Base62 encoding. - */ - private static final String BASE62_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - - /** - * A mapping of characters to their corresponding index in the Base62 character set. - */ - private static final Map CHAR_TO_INDEX_MAP = new HashMap<>(); - - /** - * Initializes the CHAR_TO_INDEX_MAP with character-to-index mappings for Base62 characters. - */ - static { - for (int i = 0; i < BASE62_CHARS.length(); i++) { - CHAR_TO_INDEX_MAP.put(BASE62_CHARS.charAt(i), i); - } - } - - /** - * Encodes a given string into a Base62-encoded string. - * - * @param data The input string to be encoded. - * @return The Base62-encoded string. - */ - public static String encode(String data) { - if ( data == null ) return null; - byte[] bytes = data.getBytes(); - StringBuilder encoded = new StringBuilder(); - long value = 0; - int bits = 0; - - for (byte b : bytes) { - value = (value << 8) | (b & 0xFF); - bits += 8; - - while (bits >= 6) { - int index = (int) ((value >> (bits - 6)) & 0x3F); - encoded.append(BASE62_CHARS.charAt(index)); - bits -= 6; - } - } - - if (bits > 0) { - int index = (int) ((value << (6 - bits)) & 0x3F); - encoded.append(BASE62_CHARS.charAt(index)); - } - - return encoded.toString(); - } - - /** - * Decodes a Base62-encoded string into its original string representation. - * - * @param base62 The Base62-encoded string to be decoded. - * @return The decoded original string. - * @throws IllegalArgumentException If the input string contains invalid Base62 characters. - */ - public static String decode(String base62) throws IllegalArgumentException { - - if ( base62 == null ) return null; - - long value = 0; - int bits = 0; - byte[] bytes = new byte[base62.length() * 6 / 8]; - int byteIndex = 0; - - for (int i = 0; i < base62.length(); i++) { - char c = base62.charAt(i); - int charValue = CHAR_TO_INDEX_MAP.getOrDefault(c, -1); - if (charValue == -1) { - throw new IllegalArgumentException("Invalid character in base62 string: " + c); - } - - value = (value << 6) | charValue; - bits += 6; - - while (bits >= 8) { - bytes[byteIndex++] = (byte) ((value >> (bits - 8)) & 0xFF); - bits -= 8; - } - } - - return new String(bytes, 0, byteIndex); - } - - /** - * Decodes a Base62-encoded string into its original string representation or returning the original string - * - * This is a way to easily recover from a double decode. This is a little soft in its approach - * and only works if the strings being encoded and decoded have non-base62 characters. - * - * @param base62 The Base62-encoded string to be decoded. - * @return The decoded original string or the original string if the input string is not Base62 - */ - public static String decodeSafe(String base62) throws IllegalArgumentException { - - if ( base62 == null ) return null; - - try { - return decode(base62); - } catch (IllegalArgumentException e) { - return base62; - } - } - -} diff --git a/basiclti/tsugi-util/src/java/org/tsugi/util/Base64DoubleUrlEncodeSafe.java b/basiclti/tsugi-util/src/java/org/tsugi/util/Base64DoubleUrlEncodeSafe.java new file mode 100644 index 000000000000..82c001dbf566 --- /dev/null +++ b/basiclti/tsugi-util/src/java/org/tsugi/util/Base64DoubleUrlEncodeSafe.java @@ -0,0 +1,55 @@ +/** + * When complex strings with URL sensitive characters are passed as URL parameters we could use + * URLEncoding, but sometimes we end up double URL Encoding and that is generally seen as a security + * problem and some firewalls remove double URL Encoding. + * + * And Base64 has a URL safe encoding - but it uses '=' for padding which does not survive double URL + * Encoding. + * + * So our solution is to manually map '=' to '.' which produces strings which do not change when URL + * Encoded - so double URL Encoding can be avoided. The use of '.' as one of the "encoded" characters + * is common in JWT encoders. Sadly, we cannot use JWT encoder and decoder because it is specialized + * for JSON so we cannot use it here. Also, since the only compatibility that JWT encoders need to work + * with are JWT decoders, so JWT dispenses with padding completely - hence you will never see an '=' + * in an encoded JWT (https://jwt.io/) + */ + +package org.tsugi.util; + +public class Base64DoubleUrlEncodeSafe { + + // https://datatracker.ietf.org/doc/html/rfc4648#section-3.2 + public static final char CHARACTER_TO_AVOID = '='; + public static final char REPLACEMENT_CHARACTER = '.'; + + /** + * Encodes a given string into a Base64-Double-UrlEncode-Safe string. + * + * @param data The input string to be encoded. + * @return The Base64-Double-UrlEncode-Safe encoded string. + */ + public static String encode(String data) { + if ( data == null ) return null; + try { + String encoded = java.util.Base64.getUrlEncoder().encodeToString(data.getBytes("UTF-8")); + return encoded.replace(CHARACTER_TO_AVOID, REPLACEMENT_CHARACTER); + } catch (java.io.UnsupportedEncodingException e ) { + // Unlikely + e.printStackTrace(); + return null; + } + } + + /** + * Decodes a Base64-Double-UrlEncode-Safe string into its original string representation. + * + * @param encoded The Base64-Double-UrlEncode-Safe string to be decoded. + * @return The decoded original string. + * @throws java.io.UnsupportedEncodingException If the input string contains invalid characters. + */ + public static String decode(String encoded) { + if ( encoded == null ) return null; + return new String(java.util.Base64.getUrlDecoder().decode(encoded.replace(REPLACEMENT_CHARACTER, CHARACTER_TO_AVOID))); + } + +} diff --git a/basiclti/tsugi-util/src/test/org/tsugi/util/Base62Test.java b/basiclti/tsugi-util/src/test/org/tsugi/util/Base62Test.java deleted file mode 100644 index 39ca54651285..000000000000 --- a/basiclti/tsugi-util/src/test/org/tsugi/util/Base62Test.java +++ /dev/null @@ -1,142 +0,0 @@ -package org.tsugi.util; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; -import static org.junit.Assert.assertFalse; - -import org.junit.Test; - -import org.tsugi.util.Base62; - -/* This code was written by ChatGPT with the following prompt - * Please write a unit test for a Base62 class - */ - -public class Base62Test { - - @Test - public void testEncodeAndDecode() { - String[] testStrings = { - "Hello, Base62!", - "Testing encoding and decoding.", - "12345", - "This is a longer string with more characters.", - "A tree 🌲 was here", - "A", - "" - }; - - for (String input : testStrings) { - String encoded = Base62.encode(input); - String decoded = Base62.decode(encoded); - assertEquals("Decoding did not produce the original string for input: " + input, input, decoded); - - try { - String urlEncoded = java.net.URLEncoder.encode(encoded, "UTF-8"); - assertEquals("UrlEncoding changed the string: " + encoded, encoded, urlEncoded); - } catch (java.io.UnsupportedEncodingException e) { - fail("Unxpected UnsupportedEncodingException for URLEncoder "+encoded); - } - } - - assertEquals("Encode of null should produce null: ", null, Base62.encode(null)); - assertEquals("Decode of null should produce null: ", null, Base62.decode(null)); - } - - @Test - public void testInvalidCharacters() { - // Test invalid characters in decoding - String invalidBase62String = "InvalidString$#@!"; - try { - Base62.decode(invalidBase62String); - fail("Expected IllegalArgumentException for invalid Base62 string"); - } catch (IllegalArgumentException e) { - // Expected exception - } - assertEquals("Encode of null should produce null: ", null, Base62.encode(null)); - } - - @Test - public void testInvalidCharactersSafe() { - // Test invalid characters in decoding - String invalidBase62String = "InvalidString$#@!"; - String decoded = Base62.decodeSafe(invalidBase62String); - - assertEquals("Safe decode of non-base22 string should return the string: ", invalidBase62String, decoded); - - String encoded = Base62.encode(invalidBase62String); - decoded = Base62.decodeSafe(encoded); - assertEquals("Safe decode of base22 string should return the decoded string: ", invalidBase62String, decoded); - } - - /** - * As is always the case, developers resist the idea of using Base62 with things like - * Base64 URL safe encoding. Which as the example below shows that whilst the resulting - * encoded strings *are* URL Encodable - they will suffer when double URL encoding happens. - */ - @Test - public void testVerifyJavaBase64UrlSafeEncodingFails() throws Exception { - String[] testStrings = { - "Hello, World!", - "12345", - "abcABC", - "A tree 🌲 was here", - "!@#$%^&*()_+{}[]|\\:;<>,.?/~`" - }; - - int failcount = 0; - - for (String input : testStrings) { - String base64UrlEncoded = java.util.Base64.getUrlEncoder().encodeToString(input.getBytes("UTF-8")); - - // URL-encode the base64 URL encoded string - String urlEncoded = java.net.URLEncoder.encode(base64UrlEncoded, "UTF-8"); - - // Compare the original encoded string with the URL-encoded string - if ( ! base64UrlEncoded.equals(urlEncoded)) failcount ++; - } - assertFalse(failcount == 0); - } - - /** - * Also suggested in code review was the serialization used by io.jsonwebtoken.io.Base64 - * which does not pad. But the writers of said code made it private on purpose - probably - * because it is not precisely Base64 because the lack of padding. - * - * This test might work if it were not for the fact that io.jsonwebtoken.io.Base64 is - * explicitly not public by its creators. Adding this import above will fail to compile: - * - * import io.jsonwebtoken.io.Base64; - * - * [ERROR] Base62Test.java:[10,25] error: Base64 is not public in io.jsonwebtoken.io; cannot be accessed from outside package - * - * The test below might work if it were not for the fact that io.jsonwebtoken.io.Base64 is - * explicitly not public by its creators. Adding this import above will fail to compile: - */ - - /* - @Test - public void testIoJsonWebTokenBase64UrlIsPrivate() throws Exception { - String[] testStrings = { - "Hello, World!", - "12345", - "abcABC", - "A tree 🌲 was here", - "!@#$%^&*()_+{}[]|\\:;<>,.?/~`" - }; - - for (String input : testStrings) { - // Encode using jjwt-api base64 URL encoding - boolean linesep = false; - String base64UrlEncoded = io.jsonwebtoken.io.Base64.URL_SAFE.encodeToString(input.getBytes("UTF-8"), linesep); - - // URL-encode the base64 URL encoded string - String urlEncoded = java.net.URLEncoder.encode(base64UrlEncoded, "UTF-8"); - - // Compare the original encoded string with the URL-encoded string - assertEquals(base64UrlEncoded, urlEncoded); - } - } - */ -} - diff --git a/basiclti/tsugi-util/src/test/org/tsugi/util/Base64DoubleUrlEncodeSafeTest.java b/basiclti/tsugi-util/src/test/org/tsugi/util/Base64DoubleUrlEncodeSafeTest.java new file mode 100644 index 000000000000..8d903764ee0e --- /dev/null +++ b/basiclti/tsugi-util/src/test/org/tsugi/util/Base64DoubleUrlEncodeSafeTest.java @@ -0,0 +1,128 @@ +package org.tsugi.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.junit.Assert.assertFalse; + +import org.junit.Test; + +import org.tsugi.util.Base64DoubleUrlEncodeSafe; + +public class Base64DoubleUrlEncodeSafeTest { + + public static String[] testStrings = { + "Hello, Base64DoubleUrlEncodeSafe!", + "Testing encoding and decoding.", + "12345", + "This is a longer string with more characters.", + "A tree 🌲 was here", + "A", + "https://www.tsugicloud.org/lti/store/?x=42&y=26", + "https://www.tsugicloud.org/lti_store/?x=42&y=26", + "https://chat.openai.com/failed/to_solve!this[problem]with{auto}generated:code;", + " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~", + " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~", + " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~", + " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~", + + // Lets throw in some mean Unicode stuff + + "ஹ௸௵꧄.ဪ꧅⸻𒈙𒐫﷽𒌄𒈟𒍼𒁎𒀱𒈓𒍙𒊎𒄡𒅌𒁏𒀰𒐪𒈙𒐫𱁬𰽔𪚥䨻龘䲜       𓀐𓂸😃⃢👍༼;´༎ຶ ۝ ༎ຶ༽", + + "hÃllo What is the weather tomorrow Göödnight 😊", + "Ḽơᶉëᶆ ȋṕšᶙṁ ḍỡḽǭᵳ ʂǐť ӓṁệẗ, ĉṓɲṩḙċťᶒțûɾ ấɖḯƥĭṩčįɳġ ḝłįʈ, șếᶑ ᶁⱺ ẽḭŭŝḿꝋď ṫĕᶆᶈṓɍ ỉñḉīḑȋᵭṵńť ṷŧ ḹẩḇőꝛế éȶ đꝍꞎôꝛȇ ᵯáꞡᶇā ąⱡîɋṹẵ.", + " ăѣ𝔠ծềſģȟᎥ𝒋ǩľḿꞑȯ𝘱𝑞𝗋𝘴ȶ𝞄𝜈ψ𝒙𝘆𝚣1234567890!@#$%^&*()-_=+[{]};:'\",<.>/?~𝘈Ḇ𝖢𝕯٤ḞԍНǏ𝙅ƘԸⲘ𝙉০Ρ𝗤Ɍ𝓢ȚЦ𝒱Ѡ𝓧ƳȤѧᖯć𝗱ễ𝑓𝙜Ⴙ𝞲𝑗𝒌ļṃʼnо𝞎𝒒ᵲꜱ𝙩ừ𝗏ŵ𝒙𝒚ź1234567890!@#$%^&*()-_=+[{]};:'\",<.>/?~АḂⲤ𝗗𝖤𝗙ꞠꓧȊ𝐉𝜥ꓡ𝑀𝑵Ǭ𝙿𝑄Ŗ𝑆𝒯𝖴𝘝𝘞ꓫŸ𝜡ả𝘢ƀ𝖼ḋếᵮℊ𝙝Ꭵ𝕛кιṃդⱺ𝓅𝘲𝕣𝖘ŧ𝑢ṽẉ𝘅ყž1234567890!@#$%^&*()-_=+[{]};:'\",<.>/?~Ѧ𝙱ƇᗞΣℱԍҤ١𝔍К𝓛𝓜ƝȎ𝚸𝑄Ṛ𝓢ṮṺƲᏔꓫ𝚈𝚭𝜶Ꮟçძ𝑒𝖿𝗀ḧ𝗂𝐣ҝɭḿ𝕟𝐨𝝔𝕢ṛ𝓼тú𝔳ẃ⤬𝝲𝗓1234567890!@#$%^&*()-_=+[{]};:'\",<.>/?~𝖠Β𝒞𝘋𝙴𝓕ĢȞỈ𝕵ꓗʟ𝙼ℕ০𝚸𝗤ՀꓢṰǓⅤ𝔚Ⲭ𝑌𝙕𝘢𝕤 – Andrew Feb 10, 2019 at 14:41", + // https://jeff.cis.cabrillo.edu/tools/homoglyphs + "Ǐ ᴛ𝕙ȋṉ𝔨 Ꮥɑ𝝹𝛂îꝒⱡů𝒔 ị𝗌 𝑎 𝝂𝕖ᵳℽ ƈθꝋȴ ŵ𝕒ɣ ṫố ɪṅⱦẽ𝑔ṛѧȶể Ṥảⱪȁ𝗶 Ꭵℼṱᴏ Ḉ𝜶𝕟ṽ𝜶𝗌, 𝘿2𝕷, ąп𝔡 Бḻáċĸѣò𝐚ȑ𝒹", + "" + }; + + // Make sure at least one of the testStrings produces a string that changes upon URLEncoding to + // prove that Java's built-in Base64 URL Safe Encoder *fails* our core use case + @Test + public void testVerifyJavaBase64UrlSafeEncodingIsNotSufficient() throws java.io.UnsupportedEncodingException { + + int failcount = 0; + for (String input : testStrings) { + String base64UrlEncoded = java.util.Base64.getUrlEncoder().encodeToString(input.getBytes("UTF-8")); + + // URL-encode the base64 URL encoded string + String urlEncoded = java.net.URLEncoder.encode(base64UrlEncoded, "UTF-8"); + + // Compare the original encoded string with the URL-encoded string + if ( ! base64UrlEncoded.equals(urlEncoded)) failcount ++; + } + assertFalse(failcount == 0); + } + + @Test + public void testEncodeAndDecode() throws java.io.UnsupportedEncodingException { + for (String input : testStrings) { + String encoded = Base64DoubleUrlEncodeSafe.encode(input); + String decoded = Base64DoubleUrlEncodeSafe.decode(encoded); + assertEquals("Decoding did not produce the original string for input: " + input, input, decoded); + + try { + String urlEncoded = java.net.URLEncoder.encode(encoded, "UTF-8"); + assertEquals("UrlEncoding changed the string: " + encoded, encoded, urlEncoded); + } catch (java.io.UnsupportedEncodingException e) { + fail("Unxpected UnsupportedEncodingException for URLEncoder "+encoded); + } + } + + assertEquals("Encode of null should produce null: ", null, Base64DoubleUrlEncodeSafe.encode(null)); + assertEquals("Decode of null should produce null: ", null, Base64DoubleUrlEncodeSafe.decode(null)); + } + + // Make sure that we never get the replacement character from a normal encode + @Test + public void testReplacementCharacter() throws java.io.UnsupportedEncodingException { + + for (String input : testStrings) { + String encoded = java.util.Base64.getUrlEncoder().encodeToString(input.getBytes("UTF-8")); + + assertEquals("Encoded strings should never contain a "+Base64DoubleUrlEncodeSafe.REPLACEMENT_CHARACTER, + encoded.indexOf(Base64DoubleUrlEncodeSafe.REPLACEMENT_CHARACTER), -1); + } + } + + // Test that invalid characters fail when decoded + @Test(expected = java.lang.IllegalArgumentException.class) + public void testDecodeInvalidCharactersFail() { + String invalidBase64DoubleUrlEncodeSafeString = "InvalidString$#@!"; + + Base64DoubleUrlEncodeSafe.decode(invalidBase64DoubleUrlEncodeSafeString); + fail("Expected IllegalArgumentException for invalid Base64DoubleUrlEncodeSafe string"); + } + + // Make a long string with every code point, up to but not including surrogate pair code points + // https://en.wikipedia.org/wiki/Universal_Character_Set_characters#Surrogates + @Test + // public void testAllNonSurrogateCodePoints() throws java.io.UnsupportedEncodingException { + public void testAllNonSurrogateCodePoints() { + char c; + int i; + StringBuffer sb = new StringBuffer(); + + for(i=1;i<0xD800;i++) { + c = (char) i; + sb.append(c); + } + + String input = sb.toString(); + String encoded = Base64DoubleUrlEncodeSafe.encode(input); + String decoded = Base64DoubleUrlEncodeSafe.decode(encoded); + assertEquals("Decoding did not produce the original string for input: " + input, input, decoded); + + try { + String urlEncoded = java.net.URLEncoder.encode(encoded, "UTF-8"); + assertEquals("UrlEncoding changed the string: " + encoded, encoded, urlEncoded); + } catch (java.io.UnsupportedEncodingException e) { + fail("Unxpected UnsupportedEncodingException for URLEncoder "+encoded); + } + + } + + +} +