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.sync.HTTPFailureException;
import org.mozilla.gecko.sync.net.SyncStorageResponse;
import org.mozilla.gecko.sync.setup.Constants;
import android.accounts.Account;
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.
Account account;
try {
final String profile = Constants.DEFAULT_PROFILE;
account = AndroidFxAccount.addAndroidAccount(activity, email, password,
serverURI, null, null, false);
profile, serverURI, null, null, false);
if (account == null) {
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.sync.HTTPFailureException;
import org.mozilla.gecko.sync.net.SyncStorageResponse;
import org.mozilla.gecko.sync.setup.Constants;
import android.accounts.Account;
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.
Account account;
try {
final String profile = Constants.DEFAULT_PROFILE;
account = AndroidFxAccount.addAndroidAccount(activity, email, password,
serverURI, result.sessionToken, result.keyFetchToken, result.verified);
serverURI, profile, result.sessionToken, result.keyFetchToken, result.verified);
if (account == null) {
throw new RuntimeException("XXX what?");
}

View File

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

View File

@ -33,17 +33,24 @@ import android.os.Bundle;
public class AndroidFxAccount implements AbstractFxAccount {
protected static final String LOG_TAG = AndroidFxAccount.class.getSimpleName();
public static final String ACCOUNT_KEY_ASSERTION = "assertion";
public static final String ACCOUNT_KEY_CERTIFICATE = "certificate";
public static final String ACCOUNT_KEY_INVALID = "invalid";
public static final String ACCOUNT_KEY_SERVERURI = "serverURI";
public static final String ACCOUNT_KEY_SESSION_TOKEN = "sessionToken";
public static final String ACCOUNT_KEY_KEY_FETCH_TOKEN = "keyFetchToken";
public static final String ACCOUNT_KEY_VERIFIED = "verified";
public static final String ACCOUNT_KEY_KA = "kA";
public static final String ACCOUNT_KEY_KB = "kB";
public static final String ACCOUNT_KEY_UNWRAPKB = "unwrapkB";
public static final String ACCOUNT_KEY_ASSERTION_KEY_PAIR = "assertionKeyPair";
public static final int CURRENT_ACCOUNT_VERSION = 2;
public static final String ACCOUNT_KEY_ACCOUNT_VERSION = "version";
public static final String ACCOUNT_KEY_PROFILE = "profile";
public static final String ACCOUNT_KEY_DESCRIPTOR = "descriptor";
public static final int CURRENT_BUNDLE_VERSION = 1;
public static final String BUNDLE_KEY_BUNDLE_VERSION = "version";
public static final String BUNDLE_KEY_ASSERTION = "assertion";
public static final String BUNDLE_KEY_CERTIFICATE = "certificate";
public static final String BUNDLE_KEY_INVALID = "invalid";
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 AccountManager accountManager;
@ -71,6 +78,105 @@ public class AndroidFxAccount implements AbstractFxAccount {
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
public byte[] getEmailUTF8() {
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
public void setQuickStretchedPW(byte[] quickStretchedPW) {
accountManager.setPassword(account, quickStretchedPW == null ? null : Utils.byte2Hex(quickStretchedPW));
@ -95,7 +210,7 @@ public class AndroidFxAccount implements AbstractFxAccount {
@Override
public String getServerURI() {
return accountManager.getUserData(account, ACCOUNT_KEY_SERVERURI);
return getBundleData(BUNDLE_KEY_SERVERURI);
}
protected byte[] getUserDataBytes(String key) {
@ -108,58 +223,57 @@ public class AndroidFxAccount implements AbstractFxAccount {
@Override
public byte[] getSessionToken() {
return getUserDataBytes(ACCOUNT_KEY_SESSION_TOKEN);
return getBundleDataBytes(BUNDLE_KEY_SESSION_TOKEN);
}
@Override
public byte[] getKeyFetchToken() {
return getUserDataBytes(ACCOUNT_KEY_KEY_FETCH_TOKEN);
return getBundleDataBytes(BUNDLE_KEY_KEY_FETCH_TOKEN);
}
@Override
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
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
public boolean isVerified() {
String data = accountManager.getUserData(account, ACCOUNT_KEY_VERIFIED);
return Boolean.valueOf(data);
return getBundleDataBoolean(BUNDLE_KEY_VERIFIED, false);
}
@Override
public void setVerified() {
accountManager.setUserData(account, ACCOUNT_KEY_VERIFIED, Boolean.valueOf(true).toString());
updateBundleValue(BUNDLE_KEY_VERIFIED, true);
}
@Override
public byte[] getKa() {
return getUserDataBytes(ACCOUNT_KEY_KA);
return getUserDataBytes(BUNDLE_KEY_KA);
}
@Override
public void setKa(byte[] kA) {
accountManager.setUserData(account, ACCOUNT_KEY_KA, Utils.byte2Hex(kA));
updateBundleValue(BUNDLE_KEY_KA, Utils.byte2Hex(kA));
}
@Override
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.
for (int i = 0; i < wrappedKb.length; 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
public byte[] getKb() {
return getUserDataBytes(ACCOUNT_KEY_KB);
return getUserDataBytes(BUNDLE_KEY_KB);
}
protected BrowserIDKeyPair generateNewAssertionKeyPair() throws GeneralSecurityException {
@ -171,35 +285,41 @@ public class AndroidFxAccount implements AbstractFxAccount {
@Override
public BrowserIDKeyPair getAssertionKeyPair() throws GeneralSecurityException {
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));
} catch (Exception e) {
// Fall through to generating a new key pair.
}
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;
}
@Override
public String getCertificate() {
return accountManager.getUserData(account, ACCOUNT_KEY_CERTIFICATE);
return getBundleData(BUNDLE_KEY_CERTIFICATE);
}
@Override
public void setCertificate(String certificate) {
accountManager.setUserData(account, ACCOUNT_KEY_CERTIFICATE, certificate);
updateBundleValue(BUNDLE_KEY_CERTIFICATE, certificate);
}
@Override
public String getAssertion() {
return accountManager.getUserData(account, ACCOUNT_KEY_ASSERTION);
return getBundleData(BUNDLE_KEY_ASSERTION);
}
@Override
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.
*/
public ExtendedJSONObject toJSONObject() {
ExtendedJSONObject o = new ExtendedJSONObject();
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));
}
ExtendedJSONObject o = unbundle();
o.put("email", account.name);
try {
o.put("emailUTF8", Utils.byte2Hex(account.name.getBytes("UTF-8")));
@ -238,7 +343,7 @@ public class AndroidFxAccount implements AbstractFxAccount {
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)
throws UnsupportedEncodingException, GeneralSecurityException {
if (email == null) {
@ -265,11 +370,17 @@ public class AndroidFxAccount implements AbstractFxAccount {
byte[] unwrapBkey = FxAccountUtils.generateUnwrapBKey(quickStretchedPW);
Bundle userdata = new Bundle();
userdata.putString(AndroidFxAccount.ACCOUNT_KEY_SERVERURI, serverURI);
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));
userdata.putString(AndroidFxAccount.ACCOUNT_KEY_VERIFIED, Boolean.valueOf(verified).toString());
userdata.putString(AndroidFxAccount.ACCOUNT_KEY_UNWRAPKB, Utils.byte2Hex(unwrapBkey));
userdata.putInt(ACCOUNT_KEY_ACCOUNT_VERSION, CURRENT_ACCOUNT_VERSION);
ExtendedJSONObject descriptor = new ExtendedJSONObject();
descriptor.put(BUNDLE_KEY_BUNDLE_VERSION, CURRENT_BUNDLE_VERSION);
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);
AccountManager accountManager = AccountManager.get(context);
@ -285,13 +396,12 @@ public class AndroidFxAccount implements AbstractFxAccount {
public boolean isValid() {
// Boolean.valueOf only returns true for the string "true"; this errors in
// the direction of marking accounts valid.
boolean invalid = Boolean.valueOf(accountManager.getUserData(account, ACCOUNT_KEY_INVALID)).booleanValue();
return !invalid;
return !getBundleDataBoolean(BUNDLE_KEY_INVALID, false);
}
@Override
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>
*/
public void forgetAccountTokens() {
accountManager.setUserData(account, ACCOUNT_KEY_SESSION_TOKEN, null);
accountManager.setUserData(account, ACCOUNT_KEY_KEY_FETCH_TOKEN, null);
ExtendedJSONObject descriptor = unbundle();
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.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.mozilla.apache.commons.codec.binary.Base64;
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);
}
}