Skip to content

Commit

Permalink
MWA 2.0 Auth Repository (#605)
Browse files Browse the repository at this point in the history
* auth repo changes only

* single account auth only with new auth repo

* cleanup

* remove debug print

* fix db migration

* cleanup

* pr feedback - rename query

* add comment
  • Loading branch information
Funkatronics authored Nov 27, 2023
1 parent 8bdf779 commit 5622b1c
Show file tree
Hide file tree
Showing 19 changed files with 408 additions and 103 deletions.
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>
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 deleteUnreferencedAccounts = super.compileStatement(
"DELETE FROM " + TABLE_ACCOUNTS +
" WHERE " + COLUMN_ACCOUNTS_ID + " NOT IN " +
"(SELECT DISTINCT " + AuthorizationsSchema.COLUMN_AUTHORIZATIONS_ACCOUNT_ID +
" FROM " + AuthorizationsSchema.TABLE_AUTHORIZATIONS + ')');
deleteUnreferencedAccounts.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
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 @@ -11,10 +11,12 @@

import androidx.annotation.NonNull;

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 +31,52 @@ 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) {
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);
publicKeysDao.deleteUnreferencedPublicKeys();

db.execSQL(AccountRecordsSchema.CREATE_TABLE_ACCOUNTS);
db.execSQL("ALTER TABLE " + AuthorizationsSchema.TABLE_AUTHORIZATIONS +
" RENAME COLUMN " + AuthorizationsSchema.COLUMN_AUTHORIZATIONS_PUBLIC_KEY_ID +
" TO " + AuthorizationsSchema.COLUMN_AUTHORIZATIONS_ACCOUNT_ID);
db.execSQL("ALTER TABLE " + AuthorizationsSchema.TABLE_AUTHORIZATIONS +
" RENAME COLUMN " + AuthorizationsSchema.COLUMN_AUTHORIZATIONS_CLUSTER +
" TO " + AuthorizationsSchema.COLUMN_AUTHORIZATIONS_CHAIN);

final List<PublicKey> publicKeys = publicKeysDao.getPublicKeys();
if (!publicKeys.isEmpty()) {
AccountRecordsDao accountRecordsDao = new AccountRecordsDao(db);
for (PublicKey publicKey : publicKeys) {
final long accountId = accountRecordsDao.insert(publicKey.publicKeyRaw,
publicKey.accountLabel, null, null, null);

// the public keys will be sorted by their id, and the new account ID will
// always be >= the existing public key ID so it is safe to update these values
// in place. For publicKey.id p(n) and accountId a(n), p(n) > p(n-1) and
// a(n) >= p(n), therefore a(n) > p(n-1). So the 'WHERE account_id = p(n)'
// condition here will not collide with previously updated entries.
db.execSQL("UPDATE " + AuthorizationsSchema.TABLE_AUTHORIZATIONS +
" SET " + AuthorizationsSchema.COLUMN_AUTHORIZATIONS_ACCOUNT_ID + " = " + accountId +
" WHERE " + AuthorizationsSchema.COLUMN_AUTHORIZATIONS_ACCOUNT_ID + " = " + publicKey.id);
}
}

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 @@ -10,6 +10,8 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.solana.mobilewalletadapter.walletlib.scenario.AuthorizedAccount;

import java.util.Arrays;
import java.util.Objects;

Expand All @@ -32,10 +34,14 @@ public class AuthRecord {
@Nullable
public final String accountLabel;

@NonNull
public final AccountRecord accountRecord;

@NonNull
public final String chain;

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

@NonNull
Expand All @@ -45,7 +51,7 @@ 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;
Expand All @@ -54,29 +60,35 @@ public class AuthRecord {

/*package*/ AuthRecord(@IntRange(from = 1) int id,
@NonNull IdentityRecord identity,
@NonNull byte[] publicKey,
@Nullable String accountLabel,
@NonNull AccountRecord accountRecord,
@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.accountRecord = accountRecord;
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 = accountRecord.publicKeyRaw;
this.accountLabel = accountRecord.accountLabel;
}

public AuthorizedAccount authorizedAccount() {
return new AuthorizedAccount(accountRecord.publicKeyRaw, accountRecord.accountLabel,
accountRecord.icon, accountRecord.chains, accountRecord.features);
}

public boolean isExpired() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.solana.mobilewalletadapter.walletlib.scenario.AuthorizedAccount;

import java.util.List;

public interface AuthRepository {
Expand All @@ -19,6 +21,7 @@ public interface AuthRepository {
@NonNull
String toAuthToken(@NonNull AuthRecord authRecord);

@Deprecated
@NonNull
AuthRecord issue(@NonNull String name,
@NonNull Uri uri,
Expand All @@ -29,6 +32,15 @@ AuthRecord issue(@NonNull String name,
@Nullable Uri walletUriBase,
@Nullable byte[] scope);

@NonNull
AuthRecord issue(@NonNull String name,
@NonNull Uri uri,
@NonNull Uri relativeIconUri,
@NonNull AuthorizedAccount account,
@NonNull String cluster,
@Nullable Uri walletUriBase,
@Nullable byte[] scope);

@Nullable
AuthRecord reissue(@NonNull AuthRecord authRecord);

Expand Down
Loading

0 comments on commit 5622b1c

Please sign in to comment.