Bug 962299 - Bundle account data into a single userdata field and add profile tracking. r=nalexander

This commit is contained in:
Richard Newman 2014-01-21 21:28:57 -08:00
parent 2546e3f69f
commit a8b7a9e312
5 changed files with 206 additions and 59 deletions

View File

@ -17,6 +17,7 @@ import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.FxAccountCreateAccoun
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.sync.HTTPFailureException; import org.mozilla.gecko.sync.HTTPFailureException;
import org.mozilla.gecko.sync.net.SyncStorageResponse; import org.mozilla.gecko.sync.net.SyncStorageResponse;
import org.mozilla.gecko.sync.setup.Constants;
import android.accounts.Account; import android.accounts.Account;
import android.accounts.AccountManager; import android.accounts.AccountManager;
@ -160,8 +161,9 @@ public class FxAccountCreateAccountActivity extends FxAccountAbstractSetupActivi
// We're on the UI thread, but it's okay to create the account here. // We're on the UI thread, but it's okay to create the account here.
Account account; Account account;
try { try {
final String profile = Constants.DEFAULT_PROFILE;
account = AndroidFxAccount.addAndroidAccount(activity, email, password, account = AndroidFxAccount.addAndroidAccount(activity, email, password,
serverURI, null, null, false); profile, serverURI, null, null, false);
if (account == null) { if (account == null) {
throw new RuntimeException("XXX what?"); throw new RuntimeException("XXX what?");
} }

View File

@ -17,6 +17,7 @@ import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.FxAccountSignInTask;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.sync.HTTPFailureException; import org.mozilla.gecko.sync.HTTPFailureException;
import org.mozilla.gecko.sync.net.SyncStorageResponse; import org.mozilla.gecko.sync.net.SyncStorageResponse;
import org.mozilla.gecko.sync.setup.Constants;
import android.accounts.Account; import android.accounts.Account;
import android.accounts.AccountManager; import android.accounts.AccountManager;
@ -129,8 +130,9 @@ public class FxAccountSignInActivity extends FxAccountAbstractSetupActivity {
// We're on the UI thread, but it's okay to create the account here. // We're on the UI thread, but it's okay to create the account here.
Account account; Account account;
try { try {
final String profile = Constants.DEFAULT_PROFILE;
account = AndroidFxAccount.addAndroidAccount(activity, email, password, account = AndroidFxAccount.addAndroidAccount(activity, email, password,
serverURI, result.sessionToken, result.keyFetchToken, result.verified); serverURI, profile, result.sessionToken, result.keyFetchToken, result.verified);
if (account == null) { if (account == null) {
throw new RuntimeException("XXX what?"); throw new RuntimeException("XXX what?");
} }

View File

@ -40,6 +40,11 @@ public interface AbstractFxAccount {
*/ */
public String getServerURI(); public String getServerURI();
/**
* @return the profile name associated with the account, such as "default".
*/
public String getProfile();
public boolean isValid(); public boolean isValid();
public void setInvalid(); public void setInvalid();

View File

@ -33,17 +33,24 @@ import android.os.Bundle;
public class AndroidFxAccount implements AbstractFxAccount { public class AndroidFxAccount implements AbstractFxAccount {
protected static final String LOG_TAG = AndroidFxAccount.class.getSimpleName(); protected static final String LOG_TAG = AndroidFxAccount.class.getSimpleName();
public static final String ACCOUNT_KEY_ASSERTION = "assertion"; public static final int CURRENT_ACCOUNT_VERSION = 2;
public static final String ACCOUNT_KEY_CERTIFICATE = "certificate"; public static final String ACCOUNT_KEY_ACCOUNT_VERSION = "version";
public static final String ACCOUNT_KEY_INVALID = "invalid"; public static final String ACCOUNT_KEY_PROFILE = "profile";
public static final String ACCOUNT_KEY_SERVERURI = "serverURI"; public static final String ACCOUNT_KEY_DESCRIPTOR = "descriptor";
public static final String ACCOUNT_KEY_SESSION_TOKEN = "sessionToken";
public static final String ACCOUNT_KEY_KEY_FETCH_TOKEN = "keyFetchToken"; public static final int CURRENT_BUNDLE_VERSION = 1;
public static final String ACCOUNT_KEY_VERIFIED = "verified"; public static final String BUNDLE_KEY_BUNDLE_VERSION = "version";
public static final String ACCOUNT_KEY_KA = "kA"; public static final String BUNDLE_KEY_ASSERTION = "assertion";
public static final String ACCOUNT_KEY_KB = "kB"; public static final String BUNDLE_KEY_CERTIFICATE = "certificate";
public static final String ACCOUNT_KEY_UNWRAPKB = "unwrapkB"; public static final String BUNDLE_KEY_INVALID = "invalid";
public static final String ACCOUNT_KEY_ASSERTION_KEY_PAIR = "assertionKeyPair"; public static final String BUNDLE_KEY_SERVERURI = "serverURI";
public static final String BUNDLE_KEY_SESSION_TOKEN = "sessionToken";
public static final String BUNDLE_KEY_KEY_FETCH_TOKEN = "keyFetchToken";
public static final String BUNDLE_KEY_VERIFIED = "verified";
public static final String BUNDLE_KEY_KA = "kA";
public static final String BUNDLE_KEY_KB = "kB";
public static final String BUNDLE_KEY_UNWRAPKB = "unwrapkB";
public static final String BUNDLE_KEY_ASSERTION_KEY_PAIR = "assertionKeyPair";
protected final Context context; protected final Context context;
protected final AccountManager accountManager; protected final AccountManager accountManager;
@ -71,6 +78,105 @@ public class AndroidFxAccount implements AbstractFxAccount {
this.accountManager = AccountManager.get(this.context); this.accountManager = AccountManager.get(this.context);
} }
protected int getAccountVersion() {
String v = accountManager.getUserData(account, ACCOUNT_KEY_ACCOUNT_VERSION);
if (v == null) {
return 0; // Implicit.
}
try {
return Integer.parseInt(v, 10);
} catch (NumberFormatException ex) {
return 0;
}
}
protected void persistBundle(ExtendedJSONObject bundle) {
accountManager.setUserData(account, ACCOUNT_KEY_DESCRIPTOR, bundle.toJSONString());
}
protected ExtendedJSONObject unbundle() {
final int version = getAccountVersion();
if (version < CURRENT_ACCOUNT_VERSION) {
// Needs upgrade. For now, do nothing.
return null;
}
if (version > CURRENT_ACCOUNT_VERSION) {
// Oh dear.
return null;
}
String bundle = accountManager.getUserData(account, ACCOUNT_KEY_DESCRIPTOR);
if (bundle == null) {
return null;
}
return unbundleAccountV1(bundle);
}
protected String getBundleData(String key) {
ExtendedJSONObject o = unbundle();
if (o == null) {
return null;
}
return o.getString(key);
}
protected boolean getBundleDataBoolean(String key, boolean def) {
ExtendedJSONObject o = unbundle();
if (o == null) {
return def;
}
Boolean b = o.getBoolean(key);
if (b == null) {
return def;
}
return b.booleanValue();
}
protected byte[] getBundleDataBytes(String key) {
ExtendedJSONObject o = unbundle();
if (o == null) {
return null;
}
return o.getByteArrayHex(key);
}
protected void updateBundleDataBytes(String key, byte[] value) {
updateBundleValue(key, value == null ? null : Utils.byte2Hex(value));
}
protected void updateBundleValue(String key, boolean value) {
ExtendedJSONObject descriptor = unbundle();
if (descriptor == null) {
return;
}
descriptor.put(key, value);
persistBundle(descriptor);
}
protected void updateBundleValue(String key, String value) {
ExtendedJSONObject descriptor = unbundle();
if (descriptor == null) {
return;
}
descriptor.put(key, value);
persistBundle(descriptor);
}
private ExtendedJSONObject unbundleAccountV1(String bundle) {
ExtendedJSONObject o;
try {
o = new ExtendedJSONObject(bundle);
} catch (Exception e) {
return null;
}
if (CURRENT_BUNDLE_VERSION == o.getIntegerSafely(BUNDLE_KEY_BUNDLE_VERSION)) {
return o;
}
return null;
}
@Override @Override
public byte[] getEmailUTF8() { public byte[] getEmailUTF8() {
try { try {
@ -81,6 +187,15 @@ public class AndroidFxAccount implements AbstractFxAccount {
} }
} }
/**
* Note that if the user clears data, an account will be left pointing to a
* deleted profile. Such is life.
*/
@Override
public String getProfile() {
return accountManager.getUserData(account, ACCOUNT_KEY_PROFILE);
}
@Override @Override
public void setQuickStretchedPW(byte[] quickStretchedPW) { public void setQuickStretchedPW(byte[] quickStretchedPW) {
accountManager.setPassword(account, quickStretchedPW == null ? null : Utils.byte2Hex(quickStretchedPW)); accountManager.setPassword(account, quickStretchedPW == null ? null : Utils.byte2Hex(quickStretchedPW));
@ -95,7 +210,7 @@ public class AndroidFxAccount implements AbstractFxAccount {
@Override @Override
public String getServerURI() { public String getServerURI() {
return accountManager.getUserData(account, ACCOUNT_KEY_SERVERURI); return getBundleData(BUNDLE_KEY_SERVERURI);
} }
protected byte[] getUserDataBytes(String key) { protected byte[] getUserDataBytes(String key) {
@ -108,58 +223,57 @@ public class AndroidFxAccount implements AbstractFxAccount {
@Override @Override
public byte[] getSessionToken() { public byte[] getSessionToken() {
return getUserDataBytes(ACCOUNT_KEY_SESSION_TOKEN); return getBundleDataBytes(BUNDLE_KEY_SESSION_TOKEN);
} }
@Override @Override
public byte[] getKeyFetchToken() { public byte[] getKeyFetchToken() {
return getUserDataBytes(ACCOUNT_KEY_KEY_FETCH_TOKEN); return getBundleDataBytes(BUNDLE_KEY_KEY_FETCH_TOKEN);
} }
@Override @Override
public void setSessionToken(byte[] sessionToken) { public void setSessionToken(byte[] sessionToken) {
accountManager.setUserData(account, ACCOUNT_KEY_SESSION_TOKEN, sessionToken == null ? null : Utils.byte2Hex(sessionToken)); updateBundleDataBytes(BUNDLE_KEY_SESSION_TOKEN, sessionToken);
} }
@Override @Override
public void setKeyFetchToken(byte[] keyFetchToken) { public void setKeyFetchToken(byte[] keyFetchToken) {
accountManager.setUserData(account, ACCOUNT_KEY_KEY_FETCH_TOKEN, keyFetchToken == null ? null : Utils.byte2Hex(keyFetchToken)); updateBundleDataBytes(BUNDLE_KEY_KEY_FETCH_TOKEN, keyFetchToken);
} }
@Override @Override
public boolean isVerified() { public boolean isVerified() {
String data = accountManager.getUserData(account, ACCOUNT_KEY_VERIFIED); return getBundleDataBoolean(BUNDLE_KEY_VERIFIED, false);
return Boolean.valueOf(data);
} }
@Override @Override
public void setVerified() { public void setVerified() {
accountManager.setUserData(account, ACCOUNT_KEY_VERIFIED, Boolean.valueOf(true).toString()); updateBundleValue(BUNDLE_KEY_VERIFIED, true);
} }
@Override @Override
public byte[] getKa() { public byte[] getKa() {
return getUserDataBytes(ACCOUNT_KEY_KA); return getUserDataBytes(BUNDLE_KEY_KA);
} }
@Override @Override
public void setKa(byte[] kA) { public void setKa(byte[] kA) {
accountManager.setUserData(account, ACCOUNT_KEY_KA, Utils.byte2Hex(kA)); updateBundleValue(BUNDLE_KEY_KA, Utils.byte2Hex(kA));
} }
@Override @Override
public void setWrappedKb(byte[] wrappedKb) { public void setWrappedKb(byte[] wrappedKb) {
byte[] unwrapKb = getUserDataBytes(ACCOUNT_KEY_UNWRAPKB); byte[] unwrapKb = getUserDataBytes(BUNDLE_KEY_UNWRAPKB);
byte[] kB = new byte[wrappedKb.length]; // We could hard-code this to be 32. byte[] kB = new byte[wrappedKb.length]; // We could hard-code this to be 32.
for (int i = 0; i < wrappedKb.length; i++) { for (int i = 0; i < wrappedKb.length; i++) {
kB[i] = (byte) (wrappedKb[i] ^ unwrapKb[i]); kB[i] = (byte) (wrappedKb[i] ^ unwrapKb[i]);
} }
accountManager.setUserData(account, ACCOUNT_KEY_KB, Utils.byte2Hex(kB)); updateBundleValue(BUNDLE_KEY_KB, Utils.byte2Hex(kB));
} }
@Override @Override
public byte[] getKb() { public byte[] getKb() {
return getUserDataBytes(ACCOUNT_KEY_KB); return getUserDataBytes(BUNDLE_KEY_KB);
} }
protected BrowserIDKeyPair generateNewAssertionKeyPair() throws GeneralSecurityException { protected BrowserIDKeyPair generateNewAssertionKeyPair() throws GeneralSecurityException {
@ -171,35 +285,41 @@ public class AndroidFxAccount implements AbstractFxAccount {
@Override @Override
public BrowserIDKeyPair getAssertionKeyPair() throws GeneralSecurityException { public BrowserIDKeyPair getAssertionKeyPair() throws GeneralSecurityException {
try { try {
String data = accountManager.getUserData(account, ACCOUNT_KEY_ASSERTION_KEY_PAIR); String data = getBundleData(BUNDLE_KEY_ASSERTION_KEY_PAIR);
return RSACryptoImplementation.fromJSONObject(new ExtendedJSONObject(data)); return RSACryptoImplementation.fromJSONObject(new ExtendedJSONObject(data));
} catch (Exception e) { } catch (Exception e) {
// Fall through to generating a new key pair. // Fall through to generating a new key pair.
} }
BrowserIDKeyPair keyPair = generateNewAssertionKeyPair(); BrowserIDKeyPair keyPair = generateNewAssertionKeyPair();
accountManager.setUserData(account, ACCOUNT_KEY_ASSERTION_KEY_PAIR, keyPair.toJSONObject().toJSONString());
ExtendedJSONObject descriptor = unbundle();
if (descriptor == null) {
descriptor = new ExtendedJSONObject();
}
descriptor.put(BUNDLE_KEY_ASSERTION_KEY_PAIR, keyPair.toJSONObject().toJSONString());
persistBundle(descriptor);
return keyPair; return keyPair;
} }
@Override @Override
public String getCertificate() { public String getCertificate() {
return accountManager.getUserData(account, ACCOUNT_KEY_CERTIFICATE); return getBundleData(BUNDLE_KEY_CERTIFICATE);
} }
@Override @Override
public void setCertificate(String certificate) { public void setCertificate(String certificate) {
accountManager.setUserData(account, ACCOUNT_KEY_CERTIFICATE, certificate); updateBundleValue(BUNDLE_KEY_CERTIFICATE, certificate);
} }
@Override @Override
public String getAssertion() { public String getAssertion() {
return accountManager.getUserData(account, ACCOUNT_KEY_ASSERTION); return getBundleData(BUNDLE_KEY_ASSERTION);
} }
@Override @Override
public void setAssertion(String assertion) { public void setAssertion(String assertion) {
accountManager.setUserData(account, ACCOUNT_KEY_ASSERTION, assertion); updateBundleValue(BUNDLE_KEY_ASSERTION, assertion);
} }
/** /**
@ -212,22 +332,7 @@ public class AndroidFxAccount implements AbstractFxAccount {
* @return JSON-object of Strings. * @return JSON-object of Strings.
*/ */
public ExtendedJSONObject toJSONObject() { public ExtendedJSONObject toJSONObject() {
ExtendedJSONObject o = new ExtendedJSONObject(); ExtendedJSONObject o = unbundle();
for (String key : new String[] {
ACCOUNT_KEY_ASSERTION,
ACCOUNT_KEY_CERTIFICATE,
ACCOUNT_KEY_SERVERURI,
ACCOUNT_KEY_SESSION_TOKEN,
ACCOUNT_KEY_INVALID,
ACCOUNT_KEY_KEY_FETCH_TOKEN,
ACCOUNT_KEY_VERIFIED,
ACCOUNT_KEY_KA,
ACCOUNT_KEY_KB,
ACCOUNT_KEY_UNWRAPKB,
ACCOUNT_KEY_ASSERTION_KEY_PAIR,
}) {
o.put(key, accountManager.getUserData(account, key));
}
o.put("email", account.name); o.put("email", account.name);
try { try {
o.put("emailUTF8", Utils.byte2Hex(account.name.getBytes("UTF-8"))); o.put("emailUTF8", Utils.byte2Hex(account.name.getBytes("UTF-8")));
@ -238,7 +343,7 @@ public class AndroidFxAccount implements AbstractFxAccount {
return o; return o;
} }
public static Account addAndroidAccount(Context context, String email, String password, public static Account addAndroidAccount(Context context, String email, String password, String profile,
String serverURI, byte[] sessionToken, byte[] keyFetchToken, boolean verified) String serverURI, byte[] sessionToken, byte[] keyFetchToken, boolean verified)
throws UnsupportedEncodingException, GeneralSecurityException { throws UnsupportedEncodingException, GeneralSecurityException {
if (email == null) { if (email == null) {
@ -265,11 +370,17 @@ public class AndroidFxAccount implements AbstractFxAccount {
byte[] unwrapBkey = FxAccountUtils.generateUnwrapBKey(quickStretchedPW); byte[] unwrapBkey = FxAccountUtils.generateUnwrapBKey(quickStretchedPW);
Bundle userdata = new Bundle(); Bundle userdata = new Bundle();
userdata.putString(AndroidFxAccount.ACCOUNT_KEY_SERVERURI, serverURI); userdata.putInt(ACCOUNT_KEY_ACCOUNT_VERSION, CURRENT_ACCOUNT_VERSION);
userdata.putString(AndroidFxAccount.ACCOUNT_KEY_SESSION_TOKEN, sessionToken == null ? null : Utils.byte2Hex(sessionToken));
userdata.putString(AndroidFxAccount.ACCOUNT_KEY_KEY_FETCH_TOKEN, keyFetchToken == null ? null : Utils.byte2Hex(keyFetchToken)); ExtendedJSONObject descriptor = new ExtendedJSONObject();
userdata.putString(AndroidFxAccount.ACCOUNT_KEY_VERIFIED, Boolean.valueOf(verified).toString()); descriptor.put(BUNDLE_KEY_BUNDLE_VERSION, CURRENT_BUNDLE_VERSION);
userdata.putString(AndroidFxAccount.ACCOUNT_KEY_UNWRAPKB, Utils.byte2Hex(unwrapBkey)); descriptor.put(BUNDLE_KEY_SERVERURI, serverURI);
descriptor.put(BUNDLE_KEY_SESSION_TOKEN, sessionToken == null ? null : Utils.byte2Hex(sessionToken));
descriptor.put(BUNDLE_KEY_KEY_FETCH_TOKEN, keyFetchToken == null ? null : Utils.byte2Hex(keyFetchToken));
descriptor.put(BUNDLE_KEY_VERIFIED, Boolean.valueOf(verified).toString());
descriptor.put(BUNDLE_KEY_UNWRAPKB, Utils.byte2Hex(unwrapBkey));
userdata.putString(ACCOUNT_KEY_DESCRIPTOR, descriptor.toJSONString());
Account account = new Account(email, FxAccountConstants.ACCOUNT_TYPE); Account account = new Account(email, FxAccountConstants.ACCOUNT_TYPE);
AccountManager accountManager = AccountManager.get(context); AccountManager accountManager = AccountManager.get(context);
@ -285,13 +396,12 @@ public class AndroidFxAccount implements AbstractFxAccount {
public boolean isValid() { public boolean isValid() {
// Boolean.valueOf only returns true for the string "true"; this errors in // Boolean.valueOf only returns true for the string "true"; this errors in
// the direction of marking accounts valid. // the direction of marking accounts valid.
boolean invalid = Boolean.valueOf(accountManager.getUserData(account, ACCOUNT_KEY_INVALID)).booleanValue(); return !getBundleDataBoolean(BUNDLE_KEY_INVALID, false);
return !invalid;
} }
@Override @Override
public void setInvalid() { public void setInvalid() {
accountManager.setUserData(account, ACCOUNT_KEY_INVALID, Boolean.valueOf(true).toString()); updateBundleValue(BUNDLE_KEY_INVALID, true);
} }
/** /**
@ -313,8 +423,13 @@ public class AndroidFxAccount implements AbstractFxAccount {
* <b>For debugging only!</b> * <b>For debugging only!</b>
*/ */
public void forgetAccountTokens() { public void forgetAccountTokens() {
accountManager.setUserData(account, ACCOUNT_KEY_SESSION_TOKEN, null); ExtendedJSONObject descriptor = unbundle();
accountManager.setUserData(account, ACCOUNT_KEY_KEY_FETCH_TOKEN, null); if (descriptor == null) {
return;
}
descriptor.remove(BUNDLE_KEY_SESSION_TOKEN);
descriptor.remove(BUNDLE_KEY_KEY_FETCH_TOKEN);
persistBundle(descriptor);
} }
/** /**

View File

@ -15,6 +15,7 @@ import org.json.simple.JSONArray;
import org.json.simple.JSONObject; import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser; import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException; import org.json.simple.parser.ParseException;
import org.mozilla.apache.commons.codec.binary.Base64;
import org.mozilla.gecko.sync.UnexpectedJSONException.BadRequiredFieldJSONException; import org.mozilla.gecko.sync.UnexpectedJSONException.BadRequiredFieldJSONException;
/** /**
@ -363,4 +364,26 @@ public class ExtendedJSONObject {
} }
} }
} }
/**
* Return a base64-encoded string value as a byte array.
*/
public byte[] getByteArrayBase64(String key) {
String s = (String) this.object.get(key);
if (s == null) {
return null;
}
return Base64.decodeBase64(s);
}
/**
* Return a hex-encoded string value as a byte array.
*/
public byte[] getByteArrayHex(String key) {
String s = (String) this.object.get(key);
if (s == null) {
return null;
}
return Utils.hex2Byte(s);
}
} }