Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MWA 2.0 Auth Repository #605

Merged
merged 8 commits into from
Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@ class MobileWalletAdapterViewModel(application: Application) : AndroidViewModel(
val keypair = getApplication<FakeWalletApplication>().keyRepository.generateKeypair()
val publicKey = keypair.public as Ed25519PublicKeyParameters
Log.d(TAG, "Generated a new keypair (pub=${publicKey.encoded.contentToString()}) for authorize request")
val accounts = arrayOf(buildAccount(publicKey.encoded, "fakewallet"))
request.request.completeWithAuthorize(accounts, null,
val account = buildAccount(publicKey.encoded, "fakewallet")
request.request.completeWithAuthorize(account, null,
request.sourceVerificationState.authorizationScope.encodeToByteArray(), null)
} else {
request.request.completeWithDecline()
Expand Down Expand Up @@ -174,8 +174,8 @@ class MobileWalletAdapterViewModel(application: Application) : AndroidViewModel(
val signInResult = SignInResult(publicKey.encoded,
siwsMessage.encodeToByteArray(), signResult.signature, "ed25519")

val accounts = arrayOf(buildAccount(publicKey.encoded, "fakewallet"))
request.request.completeWithAuthorize(accounts, null,
val account = buildAccount(publicKey.encoded, "fakewallet")
request.request.completeWithAuthorize(account, null,
request.sourceVerificationState.authorizationScope.encodeToByteArray(), signInResult)
} else {
request.request.completeWithDecline()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.solana.mobilewalletadapter.walletlib.authorization;

import android.net.Uri;

import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

/* package */ class AccountRecord {
@IntRange(from = 1)
final int id;

@NonNull
final byte[] publicKeyRaw;

@Nullable
final String accountLabel;

@Nullable
final Uri icon;

@Nullable
final String[] chains;

@Nullable
final String[] features;

AccountRecord(@IntRange(from = 1) int id,
@NonNull byte[] publicKeyRaw,
@Nullable String accountLabel,
@Nullable Uri icon,
@Nullable String[] chains,
@Nullable String[] features) {
this.id = id;
this.publicKeyRaw = publicKeyRaw;
this.accountLabel = accountLabel;
this.icon = icon;
this.chains = chains;
this.features = features;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package com.solana.mobilewalletadapter.walletlib.authorization;

import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteCursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteStatement;
import android.net.Uri;
import android.text.TextUtils;

import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

public class AccountRecordsDao extends DbContentProvider<AccountRecord>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summarizing my understanding:

We're changing our "account" model from PublicKeyRecord -> AccountRecord because in MWA 2.0, the "account" model needs to be expanded to include chains/features/etc. A public key is now a part of the larger AccountRecord model.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep exactly. adding new account metadata fields to the database.

having this already done will make it easier to migrate to multi-account too. we still need to store a list of AccountRecords for each authorization, but at least now the AccountRecord itself is there.

implements AccountRecordsDaoInterface, AccountRecordsSchema {

public AccountRecordsDao(SQLiteDatabase db) { super(db); }

@NonNull
@Override
protected AccountRecord cursorToEntity(@NonNull Cursor cursor) {
final int publicKeyId = cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_ACCOUNTS_ID));
final byte[] publicKey = cursor.getBlob(cursor.getColumnIndexOrThrow(COLUMN_ACCOUNTS_PUBLIC_KEY_RAW));
final String accountLabel = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_ACCOUNTS_LABEL));
final String accountIconStr = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_ACCOUNTS_ICON));
final String chainsString = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_ACCOUNTS_CHAINS));
final String featuresString = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_ACCOUNTS_FEATURES));
final Uri accountIcon = Uri.parse(accountIconStr);
final String[] chains = deserialize(chainsString);
final String[] features = deserialize(featuresString);
return new AccountRecord(publicKeyId, publicKey, accountLabel, accountIcon, chains, features);
}

@Override
public long insert(@NonNull byte[] publicKey,
@Nullable String accountLabel,
@Nullable Uri accountIcon,
@Nullable String[] chains,
@Nullable String[] features) {
final ContentValues accountContentValues = new ContentValues(4);
accountContentValues.put(COLUMN_ACCOUNTS_PUBLIC_KEY_RAW, publicKey);
accountContentValues.put(COLUMN_ACCOUNTS_LABEL, accountLabel);
accountContentValues.put(COLUMN_ACCOUNTS_ICON, accountIcon != null ? accountIcon.toString() : null);
accountContentValues.put(COLUMN_ACCOUNTS_CHAINS, chains != null ? serialize(chains) : null);
accountContentValues.put(COLUMN_ACCOUNTS_FEATURES, features != null ? serialize(features) : null);
return super.insert(TABLE_ACCOUNTS, accountContentValues);
}

@Nullable
@Override
public AccountRecord query(@NonNull byte[] publicKey) {
final SQLiteDatabase.CursorFactory accountCursorFactory = (db1, masterQuery, editTable, query) -> {
query.bindBlob(1, publicKey);
return new SQLiteCursor(masterQuery, editTable, query);
};
try (final Cursor cursor = super.queryWithFactory(accountCursorFactory,
TABLE_ACCOUNTS,
ACCOUNTS_COLUMNS,
COLUMN_ACCOUNTS_PUBLIC_KEY_RAW + "=?",
null)) {
if (!cursor.moveToNext()) {
return null;
}
return cursorToEntity(cursor);
}
}

@Override
public void deleteUnreferencedAccounts() {
final SQLiteStatement deleteUnreferencedPublicKeys = super.compileStatement(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deleteUnreferencedPublicKeys should be renamed to deleteUnreferencedAccounts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

"DELETE FROM " + TABLE_ACCOUNTS +
" WHERE " + COLUMN_ACCOUNTS_ID + " NOT IN " +
"(SELECT DISTINCT " + AuthorizationsSchema.COLUMN_AUTHORIZATIONS_ACCOUNT_ID +
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Another problematic spot for migrated DBs in deleteUnreferencedAccounts.

COLUMN_ACCOUNTS_ID is expecting accountIds but AuthorizationsSchema.COLUMN_AUTHORIZATIONS_ACCOUNT_ID are still publicKeyIds.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed with migration from public_key_id column to account_id

" FROM " + AuthorizationsSchema.TABLE_AUTHORIZATIONS + ')');
deleteUnreferencedPublicKeys.executeUpdateDelete();
}

// using a long alphanumeric divider reduces the chance of an array element matching the divider
private static final String ARRAY_DIVIDER = "#a1r2ra5yd2iv1i9der";

private String serialize(String[] content){ return TextUtils.join(ARRAY_DIVIDER, content); }

private static String[] deserialize(String content){
return content.split(ARRAY_DIVIDER);
}

/*package*/ static AccountRecord buildAccountRecordFromRaw(@IntRange(from = 1) int id,
@NonNull byte[] publicKeyRaw,
@Nullable String accountLabel,
@Nullable String iconStr,
@Nullable String chainsStr,
@Nullable String featuresStr) {
final Uri icon = iconStr != null ? Uri.parse(iconStr) : null;
final String[] chains = chainsStr != null ? deserialize(chainsStr) : null;
final String[] features = featuresStr != null ? deserialize(featuresStr) : null;
return new AccountRecord(id, publicKeyRaw, accountLabel, icon, chains, features);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.solana.mobilewalletadapter.walletlib.authorization;

import android.net.Uri;

import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

/*package*/ interface AccountRecordsDaoInterface {

@IntRange(from = -1)
long insert(@NonNull byte[] publicKey, @Nullable String accountLabel, @Nullable Uri accountIcon,
@Nullable String[] chains, @Nullable String[] features);

@Nullable
AccountRecord query(@NonNull byte[] publicKey);

void deleteUnreferencedAccounts();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.solana.mobilewalletadapter.walletlib.authorization;

/*package*/ interface AccountRecordsSchema {
String TABLE_ACCOUNTS = "accounts";
String COLUMN_ACCOUNTS_ID = "id"; // type: long
String COLUMN_ACCOUNTS_PUBLIC_KEY_RAW = "public_key_raw"; // type: byte[]
String COLUMN_ACCOUNTS_LABEL = "label"; // type: String
String COLUMN_ACCOUNTS_ICON = "icon"; // type: String
String COLUMN_ACCOUNTS_CHAINS = "chains"; // type: String
Michaelsulistio marked this conversation as resolved.
Show resolved Hide resolved
String COLUMN_ACCOUNTS_FEATURES = "features"; // type: String

String CREATE_TABLE_ACCOUNTS =
"CREATE TABLE " + TABLE_ACCOUNTS + " (" +
COLUMN_ACCOUNTS_ID + " INTEGER NOT NULL PRIMARY KEY," +
COLUMN_ACCOUNTS_PUBLIC_KEY_RAW + " BLOB NOT NULL," +
COLUMN_ACCOUNTS_LABEL + " TEXT," +
COLUMN_ACCOUNTS_ICON + " TEXT," +
COLUMN_ACCOUNTS_CHAINS + " TEXT," +
COLUMN_ACCOUNTS_FEATURES + " TEXT)";

String[] ACCOUNTS_COLUMNS = new String[]{
COLUMN_ACCOUNTS_ID,
COLUMN_ACCOUNTS_PUBLIC_KEY_RAW,
COLUMN_ACCOUNTS_LABEL,
COLUMN_ACCOUNTS_ICON,
COLUMN_ACCOUNTS_CHAINS,
COLUMN_ACCOUNTS_FEATURES
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,20 @@
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Base64;
import android.util.Log;

import androidx.annotation.NonNull;

import com.solana.mobilewalletadapter.common.util.JsonPack;

import java.util.Arrays;
import java.util.List;

/*package*/ class AuthDatabase extends SQLiteOpenHelper {
private static final String TAG = AuthDatabase.class.getSimpleName();
private static final String DATABASE_NAME_SUFFIX = "-solana-wallet-lib-auth.db";
private static final int DATABASE_SCHEMA_VERSION = 5;
private static final int DATABASE_SCHEMA_VERSION = 6;

AuthDatabase(@NonNull Context context, @NonNull AuthIssuerConfig authIssuerConfig) {
super(context, getDatabaseName(authIssuerConfig), null, DATABASE_SCHEMA_VERSION);
Expand All @@ -29,18 +35,36 @@ public void onConfigure(SQLiteDatabase db) {
public void onCreate(SQLiteDatabase db) {
db.execSQL(IdentityRecordSchema.CREATE_TABLE_IDENTITIES);
db.execSQL(AuthorizationsSchema.CREATE_TABLE_AUTHORIZATIONS);
db.execSQL(PublicKeysSchema.CREATE_TABLE_PUBLIC_KEYS);
db.execSQL(AccountRecordsSchema.CREATE_TABLE_ACCOUNTS);
db.execSQL(WalletUriBaseSchema.CREATE_TABLE_WALLET_URI_BASE);
}

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When does onUpgrade trigger? Or, in other words, how does walletlib know when to onUpgrade the database?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onUpgrade is called whenever the database schema version changes (5->6 in this case)

Log.w(TAG, "Old database schema detected; pre-v1.0.0, no DB schema backward compatibility is implemented");
db.execSQL("DROP TABLE IF EXISTS " + IdentityRecordSchema.TABLE_IDENTITIES);
db.execSQL("DROP TABLE IF EXISTS " + AuthorizationsSchema.TABLE_AUTHORIZATIONS);
db.execSQL("DROP TABLE IF EXISTS " + PublicKeysSchema.TABLE_PUBLIC_KEYS);
db.execSQL("DROP TABLE IF EXISTS " + WalletUriBaseSchema.TABLE_WALLET_URI_BASE);
onCreate(db);
if (oldVersion < 5) {
Log.w(TAG, "Old database schema detected; pre-v1.0.0, no DB schema backward compatibility is implemented");
db.execSQL("DROP TABLE IF EXISTS " + IdentityRecordSchema.TABLE_IDENTITIES);
db.execSQL("DROP TABLE IF EXISTS " + AuthorizationsSchema.TABLE_AUTHORIZATIONS);
db.execSQL("DROP TABLE IF EXISTS " + PublicKeysSchema.TABLE_PUBLIC_KEYS);
db.execSQL("DROP TABLE IF EXISTS " + WalletUriBaseSchema.TABLE_WALLET_URI_BASE);
onCreate(db);
} else {
Log.w(TAG, "Old database schema detected; pre-v2.0.0, migrating public keys to account records");
final PublicKeysDao publicKeysDao = new PublicKeysDao(db);
final List<PublicKey> publicKeys = publicKeysDao.getAuthorizedPublicKeys();

db.execSQL(AccountRecordsSchema.CREATE_TABLE_ACCOUNTS);
if (!publicKeys.isEmpty()) {
AccountRecordsDao accountRecordsDao = new AccountRecordsDao(db);
for (PublicKey publicKey : publicKeys) {
Log.d(TAG, "migrating Public Key: " + publicKey.accountLabel + " (" + Base64.encodeToString(publicKey.publicKeyRaw, Base64.NO_WRAP) + ")");
accountRecordsDao.insert(publicKey.publicKeyRaw, publicKey.accountLabel,
null, null, null);
}
}

db.execSQL("DROP TABLE IF EXISTS " + PublicKeysSchema.TABLE_PUBLIC_KEYS);
}
}

@NonNull
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,22 @@ public class AuthRecord {
@IntRange(from = 0)
public final long expires;

@Deprecated
@NonNull
public final byte[] publicKey;

@Deprecated
@Nullable
public final String accountLabel;

@NonNull
public final AccountRecord account;

@NonNull
public final String chain;

@NonNull @Deprecated
@Deprecated
@NonNull
public final String cluster;

@NonNull
Expand All @@ -45,38 +51,66 @@ public class AuthRecord {
public final Uri walletUriBase;

@IntRange(from = 1)
/*package*/ final int publicKeyId;
/*package*/ final int accountId;

@IntRange(from = 1)
/*package*/ final int walletUriBaseId;

private boolean mRevoked;

// /*package*/ AuthRecord(@IntRange(from = 1) int id,
// @NonNull IdentityRecord identity,
// @NonNull byte[] publicKey,
// @Nullable String accountLabel,
// @NonNull String chain,
// @NonNull byte[] scope,
// @Nullable Uri walletUriBase,
// @IntRange(from = 1) int publicKeyId,
// @IntRange(from = 1) int walletUriBaseId,
// @IntRange(from = 0) long issued,
// @IntRange(from = 0) long expires) {
// // N.B. This is a package-visibility constructor; these values will all be validated by
// // other components within this package.
// this.id = id;
// this.identity = identity;
// this.publicKey = publicKey;
// this.accountLabel = accountLabel;
// this.chain = chain;
// this.cluster = chain;
// this.scope = scope;
// this.walletUriBase = walletUriBase;
// this.publicKeyId = publicKeyId;
// this.walletUriBaseId = walletUriBaseId;
// this.issued = issued;
// this.expires = expires;
// }

/*package*/ AuthRecord(@IntRange(from = 1) int id,
@NonNull IdentityRecord identity,
@NonNull byte[] publicKey,
@Nullable String accountLabel,
@NonNull AccountRecord account,
@NonNull String chain,
@NonNull byte[] scope,
@Nullable Uri walletUriBase,
@IntRange(from = 1) int publicKeyId,
@IntRange(from = 1) int accountId,
@IntRange(from = 1) int walletUriBaseId,
@IntRange(from = 0) long issued,
@IntRange(from = 0) long expires) {
// N.B. This is a package-visibility constructor; these values will all be validated by
// other components within this package.
this.id = id;
this.identity = identity;
this.publicKey = publicKey;
this.accountLabel = accountLabel;
this.account = account;
this.chain = chain;
this.cluster = chain;
this.scope = scope;
this.walletUriBase = walletUriBase;
this.publicKeyId = publicKeyId;
this.accountId = accountId;
this.walletUriBaseId = walletUriBaseId;
this.issued = issued;
this.expires = expires;

this.publicKey = account.publicKeyRaw;
this.accountLabel = account.accountLabel;
}

public boolean isExpired() {
Expand Down
Loading
Loading