From 724c3a81cd3649b48ab47c6e49cb42f73f20c815 Mon Sep 17 00:00:00 2001 From: Ben Komalo Date: Wed, 8 Jun 2011 11:38:36 -0700 Subject: [PATCH] Introduce scheme name escaping in SSLUtils. Change-Id: I73f19e7d40d0b19dfd41cfaf7db0879ef2e3a3ea --- .../emailcommon/provider/HostAuth.java | 6 +- .../android/emailcommon/utility/SSLUtils.java | 28 ++++++ .../emailcommon/provider/HostAuthTests.java | 10 +-- .../emailcommon/utility/SSLUtilsTest.java | 87 +++++++++++++++++++ 4 files changed, 122 insertions(+), 9 deletions(-) create mode 100644 tests/src/com/android/emailcommon/utility/SSLUtilsTest.java diff --git a/emailcommon/src/com/android/emailcommon/provider/HostAuth.java b/emailcommon/src/com/android/emailcommon/provider/HostAuth.java index 61d688b35..88884fd08 100644 --- a/emailcommon/src/com/android/emailcommon/provider/HostAuth.java +++ b/emailcommon/src/com/android/emailcommon/provider/HostAuth.java @@ -18,6 +18,7 @@ package com.android.emailcommon.provider; import com.android.emailcommon.provider.EmailContent.HostAuthColumns; +import com.android.emailcommon.utility.SSLUtils; import com.android.emailcommon.utility.Utility; import android.content.ContentValues; @@ -133,13 +134,10 @@ public final class HostAuth extends EmailContent implements HostAuthColumns, Par throw new IllegalArgumentException( "Can't specify a certificate alias for a non-secure connection"); } - // TODO: investigate what the certificate aliases look like from the framework - // and ensure they're safe scheme names. - String safeScheme = clientAlias; if (!security.endsWith("+")) { security += "+"; } - security += safeScheme; + security += SSLUtils.escapeForSchemeName(clientAlias); } return protocol + security; diff --git a/emailcommon/src/com/android/emailcommon/utility/SSLUtils.java b/emailcommon/src/com/android/emailcommon/utility/SSLUtils.java index 4869a0f6a..8ac242cb2 100644 --- a/emailcommon/src/com/android/emailcommon/utility/SSLUtils.java +++ b/emailcommon/src/com/android/emailcommon/utility/SSLUtils.java @@ -42,4 +42,32 @@ public class SSLUtils { return sSecureFactory; } } + + /** + * Escapes the contents a string to be used as a safe scheme name in the URI according to + * http://tools.ietf.org/html/rfc3986#section-3.1 + * + * This does not ensure that the first character is a letter (which is required by the RFC). + */ + public static String escapeForSchemeName(String s) { + // According to the RFC, scheme names are case-insensitive. + s = s.toLowerCase(); + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (Character.isLetter(c) || Character.isDigit(c) + || ('-' == c) || ('.' == c)) { + // Safe - use as is. + sb.append(c); + } else if ('+' == c) { + // + is used as our escape character, so double it up. + sb.append("++"); + } else { + // Unsafe - escape. + sb.append('+').append((int) c); + } + } + return sb.toString(); + } } diff --git a/tests/src/com/android/emailcommon/provider/HostAuthTests.java b/tests/src/com/android/emailcommon/provider/HostAuthTests.java index c0edf322f..0665e6c58 100644 --- a/tests/src/com/android/emailcommon/provider/HostAuthTests.java +++ b/tests/src/com/android/emailcommon/provider/HostAuthTests.java @@ -450,15 +450,15 @@ public class HostAuthTests extends AndroidTestCase { scheme = HostAuth.getSchemeString("foo", HostAuth.FLAG_TLS, "custom-client-cert-alias"); assertEquals("foo+tls+custom-client-cert-alias", scheme); scheme = HostAuth.getSchemeString( - "foo", HostAuth.FLAG_SSL | HostAuth.FLAG_TRUST_ALL, "custom_client_cert_alias"); - assertEquals("foo+ssl+trustallcerts+custom_client_cert_alias", scheme); + "foo", HostAuth.FLAG_SSL | HostAuth.FLAG_TRUST_ALL, "custom-client-cert-alias"); + assertEquals("foo+ssl+trustallcerts+custom-client-cert-alias", scheme); scheme = HostAuth.getSchemeString( - "foo", HostAuth.FLAG_TLS | HostAuth.FLAG_TRUST_ALL, "custom_client_cert_alias"); - assertEquals("foo+tls+trustallcerts+custom_client_cert_alias", scheme); + "foo", HostAuth.FLAG_TLS | HostAuth.FLAG_TRUST_ALL, "custom-client-cert-alias"); + assertEquals("foo+tls+trustallcerts+custom-client-cert-alias", scheme); try { scheme = HostAuth.getSchemeString( - "foo", 0 /* no security flags */, "custom_client_cert_alias"); + "foo", 0 /* no security flags */, "custom-client-cert-alias"); fail("Should not be able to set a custom client cert on an insecure connection"); } catch (IllegalArgumentException expected) { } diff --git a/tests/src/com/android/emailcommon/utility/SSLUtilsTest.java b/tests/src/com/android/emailcommon/utility/SSLUtilsTest.java new file mode 100644 index 000000000..d373b9c3a --- /dev/null +++ b/tests/src/com/android/emailcommon/utility/SSLUtilsTest.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.emailcommon.utility; + +import android.test.AndroidTestCase; +import android.test.MoreAsserts; +import android.test.suitebuilder.annotation.SmallTest; + +import java.util.Random; +import java.util.regex.Pattern; + +/** + * Unit tests for SSLUtils. + */ +@SmallTest +public class SSLUtilsTest extends AndroidTestCase { + + String SAFE_SCHEME_PATTERN = "[a-z][a-z0-9+\\-]*"; + private void assertSchemeNameValid(String s) { + assertTrue(Pattern.matches(SAFE_SCHEME_PATTERN, s)); + } + + public void testSchemeNameEscapeAlreadySafe() { + // Safe names are unmodified. + assertEquals("http", SSLUtils.escapeForSchemeName("http")); + assertEquals("https", SSLUtils.escapeForSchemeName("https")); + assertEquals("ftp", SSLUtils.escapeForSchemeName("ftp")); + assertEquals("z39.50r", SSLUtils.escapeForSchemeName("z39.50r")); + assertEquals("fake-protocol.yes", SSLUtils.escapeForSchemeName("fake-protocol.yes")); + } + + public void testSchemeNameEscapeIsSafe() { + // Invalid characters are escaped properly + assertSchemeNameValid(SSLUtils.escapeForSchemeName("name with spaces")); + assertSchemeNameValid(SSLUtils.escapeForSchemeName("odd * & characters")); + assertSchemeNameValid(SSLUtils.escapeForSchemeName("f3v!l;891023-47 +")); + } + + private static final char[] RANDOM_DICT = new char[] { + 'x', '.', '^', '4', ';', ' ', 'j', '#', '~', '+' + }; + private String randomString(Random r) { + // 5 to 15 characters + int length = (r.nextInt() % 5) + 10; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + sb.append(RANDOM_DICT[Math.abs(r.nextInt()) % RANDOM_DICT.length]); + } + return sb.toString(); + } + + public void testSchemeNamesAreMoreOrLessUnique() { + assertEquals( + SSLUtils.escapeForSchemeName("name with spaces"), + SSLUtils.escapeForSchemeName("name with spaces")); + + // As expected, all escaping is case insensitive. + assertEquals( + SSLUtils.escapeForSchemeName("NAME with spaces"), + SSLUtils.escapeForSchemeName("name with spaces")); + + Random random = new Random(314159 /* seed */); + for (int i = 0; i < 100; i++) { + // Other strings should more or less be unique. + String s1 = randomString(random); + String s2 = randomString(random); + MoreAsserts.assertNotEqual( + SSLUtils.escapeForSchemeName(s1), + SSLUtils.escapeForSchemeName(s2)); + } + } +} +