Skip to content
This repository
Browse code

Bug 774300 - Sync authentication errors if passwords contain non-ASCI…

…I characters. r=nalexander
  • Loading branch information...
commit 5dc6bf466ad326682ee2f287d8528c2e33dc7972 1 parent 6244f6a
Richard Newman authored July 31, 2012
18  src/main/java/org/mozilla/gecko/sync/Utils.java
@@ -504,4 +504,20 @@ public static String formatDuration(long startMillis, long endMillis) {
504 504
     final long duration = endMillis - startMillis;
505 505
     return new DecimalFormat("#0.00 seconds").format(((double) duration) / 1000);
506 506
   }
507  
-}
  507
+
  508
+  /**
  509
+   * This will take a string containing a UTF-8 representation of a UTF-8
  510
+   * byte array — e.g., "pïgéons1" — and return UTF-8 (e.g., "pïgéons1").
  511
+   *
  512
+   * This is the format produced by desktop Firefox when exchanging credentials
  513
+   * containing non-ASCII characters.
  514
+   */
  515
+  public static String decodeUTF8(final String in) throws UnsupportedEncodingException {
  516
+    final int length = in.length();
  517
+    final byte[] asciiBytes = new byte[length];
  518
+    for (int i = 0; i < length; ++i) {
  519
+      asciiBytes[i] = (byte) in.codePointAt(i);
  520
+    }
  521
+    return new String(asciiBytes, "UTF-8");
  522
+  }
  523
+}
12  src/main/java/org/mozilla/gecko/sync/net/BaseResource.java
@@ -130,13 +130,19 @@ private static void addAuthCacheToContext(HttpUriRequest request, HttpContext co
130 130
   }
131 131
 
132 132
   /**
  133
+   * Return a Header object representing an Authentication header for HTTP Basic.
  134
+   */
  135
+  public static Header getBasicAuthHeader(final String credentials) {
  136
+    Credentials creds = new UsernamePasswordCredentials(credentials);
  137
+    return BasicScheme.authenticate(creds, "US-ASCII", false);
  138
+  }
  139
+
  140
+  /**
133 141
    * Apply the provided credentials string to the provided request.
134 142
    * @param credentials a string, "user:pass".
135 143
    */
136 144
   private static void applyCredentials(String credentials, HttpUriRequest request, HttpContext context) {
137  
-    Credentials creds = new UsernamePasswordCredentials(credentials);
138  
-    Header header = BasicScheme.authenticate(creds, "US-ASCII", false);
139  
-    request.addHeader(header);
  145
+    request.addHeader(getBasicAuthHeader(credentials));
140 146
     Logger.trace(LOG_TAG, "Adding Basic Auth header.");
141 147
   }
142 148
 
9  src/main/java/org/mozilla/gecko/sync/setup/activities/SetupSyncActivity.java
@@ -4,6 +4,7 @@
4 4
 
5 5
 package org.mozilla.gecko.sync.setup.activities;
6 6
 
  7
+import java.io.UnsupportedEncodingException;
7 8
 import java.util.HashMap;
8 9
 
9 10
 import org.json.simple.JSONObject;
@@ -11,6 +12,7 @@
11 12
 import org.mozilla.gecko.sync.GlobalConstants;
12 13
 import org.mozilla.gecko.sync.Logger;
13 14
 import org.mozilla.gecko.sync.ThreadPool;
  15
+import org.mozilla.gecko.sync.Utils;
14 16
 import org.mozilla.gecko.sync.jpake.JPakeClient;
15 17
 import org.mozilla.gecko.sync.jpake.JPakeNoActivePairingException;
16 18
 import org.mozilla.gecko.sync.setup.Constants;
@@ -392,6 +394,13 @@ public void onComplete(JSONObject jCreds) {
392 394
       String syncKey      = (String) jCreds.get(Constants.JSON_KEY_SYNCKEY);
393 395
       String serverURL    = (String) jCreds.get(Constants.JSON_KEY_SERVER);
394 396
 
  397
+      // The password we get is double-encoded.
  398
+      try {
  399
+        password = Utils.decodeUTF8(password);
  400
+      } catch (UnsupportedEncodingException e) {
  401
+        Logger.warn(LOG_TAG, "Unsupported encoding when decoding UTF-8 ASCII J-PAKE message. Ignoring.");
  402
+      }
  403
+
395 404
       final SyncAccountParameters syncAccount = new SyncAccountParameters(mContext, mAccountManager, accountName,
396 405
                                                                           syncKey, password, serverURL);
397 406
       createAccountOnThread(syncAccount);
68  src/test/java/org/mozilla/android/sync/net/test/TestCredentialsEndToEnd.java
... ...
@@ -0,0 +1,68 @@
  1
+/* Any copyright is dedicated to the Public Domain.
  2
+   http://creativecommons.org/publicdomain/zero/1.0/ */
  3
+
  4
+package org.mozilla.android.sync.net.test;
  5
+
  6
+import static org.junit.Assert.assertEquals;
  7
+
  8
+import java.io.IOException;
  9
+import java.io.UnsupportedEncodingException;
  10
+
  11
+import org.json.simple.parser.ParseException;
  12
+import org.junit.Test;
  13
+import org.mozilla.gecko.sync.ExtendedJSONObject;
  14
+import org.mozilla.gecko.sync.NonObjectJSONException;
  15
+import org.mozilla.gecko.sync.Utils;
  16
+import org.mozilla.gecko.sync.net.BaseResource;
  17
+
  18
+import ch.boye.httpclientandroidlib.Header;
  19
+
  20
+/**
  21
+ * Test the transfer of a UTF-8 string from desktop, and ensure that it results in the
  22
+ * correct hashed Basic Auth header.
  23
+ */
  24
+public class TestCredentialsEndToEnd {
  25
+
  26
+  public static final String REAL_PASSWORD         = "pïgéons1";
  27
+  public static final String USERNAME              = "utvm3mk6hnngiir2sp4jsxf2uvoycrv6";
  28
+  public static final String DESKTOP_PASSWORD_JSON = "{\"password\":\"pïgéons1\"}";
  29
+  public static final String BTOA_PASSWORD         = "cMOvZ8Opb25zMQ==";
  30
+  public static final int    DESKTOP_ASSERTED_SIZE = 10;
  31
+  public static final String DESKTOP_BASIC_AUTH    = "Basic dXR2bTNtazZobm5naWlyMnNwNGpzeGYydXZveWNydjY6cMOvZ8Opb25zMQ==";
  32
+
  33
+  private String getCreds(String password) {
  34
+    Header authenticate = BaseResource.getBasicAuthHeader(USERNAME + ":" + password);
  35
+    return authenticate.getValue();
  36
+  }
  37
+
  38
+  @Test
  39
+  public void testUTF8() throws UnsupportedEncodingException {
  40
+    final String in  = "pïgéons1";
  41
+    final String out = "pïgéons1";
  42
+    assertEquals(out, Utils.decodeUTF8(in));
  43
+  }
  44
+
  45
+  @Test
  46
+  public void testAuthHeaderFromPassword() throws NonObjectJSONException, IOException, ParseException {
  47
+    final ExtendedJSONObject parsed = new ExtendedJSONObject(DESKTOP_PASSWORD_JSON);
  48
+
  49
+    final String password = parsed.getString("password");
  50
+    final String decoded = Utils.decodeUTF8(password);
  51
+
  52
+    final byte[] expectedBytes = Utils.decodeBase64(BTOA_PASSWORD);
  53
+    final String expected = new String(expectedBytes, "UTF-8");
  54
+
  55
+    assertEquals(DESKTOP_ASSERTED_SIZE, password.length());
  56
+    assertEquals(expected, decoded);
  57
+
  58
+    System.out.println("Retrieved password: " + password);
  59
+    System.out.println("Expected password:  " + expected);
  60
+    System.out.println("Rescued password:   " + decoded);
  61
+
  62
+    assertEquals(getCreds(expected), getCreds(decoded));
  63
+  }
  64
+
  65
+  // Note that we do *not* have a test for the J-PAKE setup process
  66
+  // (SetupSyncActivity) that actually stores credentials and requires
  67
+  // decodeUTF8. This will have to suffice.
  68
+}

0 notes on commit 5dc6bf4

Please sign in to comment.
Something went wrong with that request. Please try again.