Skip to content

Commit

Permalink
fix: Integrate VeraId in account creation process (#43)
Browse files Browse the repository at this point in the history
LTR-60

# High-level changes

- Migrated to production environment (by updating connection params in `server_connection_params.der` file).
- When communicating with the server, instead of requesting a VeraId id (e.g., `[email protected]`), we now request a user name (e.g., `alice`) in the current locale (e.g., `en_GB`) -- the user may be running a (very) old version of the Letro app, which may not reflect the latest set of domain names supported by Relaycorp's Letro server.
- Renamed instances of "veraId" to "veraidId" for legal reasons (we have the trademark for "VeraId", not "Vera", which belongs to a third party). I didn't change all occurrences -- only the ones I worked with.
- I removed 2-3 classes related to the `AwalaMessageParser` implementation for account creation messages, as they seemed unnecessary.

## Changes to ignore

I'd recommend ignoring the following packages during review:

-  `tech.relaycorp.letro.server.messages`: ASN.1 DER serialisation of [Letro server](https://docs.relaycorp.tech/letro-server/) messages, such as [account creation](https://docs.relaycorp.tech/letro-server/account-creation); the final version of the contact pairing serialisers will be moved there.
- `tech.relaycorp.letro.utils`: Various Letro-agnostic utilities related to ASN.1, cryptography, etc.
  • Loading branch information
gnarea authored Sep 25, 2023
1 parent a37e63d commit 1f6eb3e
Show file tree
Hide file tree
Showing 46 changed files with 1,708 additions and 164 deletions.
21 changes: 19 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ android {
lintConfig file('lint.xml')
warningsAsErrors true
}
testOptions {
unitTests.all {
useJUnitPlatform()
}
}
}

dependencies {
Expand All @@ -65,8 +70,14 @@ dependencies {
implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'

// Awala
// Awala-powered messaging
implementation 'com.github.relaycorp:awala-endpoint-android:1.13.23'
def bouncy_castle_version = "1.70"
implementation "org.bouncycastle:bcprov-jdk15on:$bouncy_castle_version"
implementation "org.bouncycastle:bcpkix-jdk15on:$bouncy_castle_version"

// VeraId
implementation 'tech.relaycorp:veraid:1.10.0'

// Compose
implementation platform('androidx.compose:compose-bom:2023.09.00')
Expand Down Expand Up @@ -99,13 +110,19 @@ dependencies {


// Testing
testImplementation 'junit:junit:4.13.2'
def junitVersion = "5.8.2"
def kotlinCoroutinesVersion = "1.7.3"
testImplementation "org.junit.jupiter:junit-jupiter:$junitVersion"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation platform('androidx.compose:compose-bom:2023.06.01')
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
debugImplementation 'androidx.compose.ui:ui-tooling'
debugImplementation 'androidx.compose.ui:ui-test-manifest'
testImplementation 'io.kotest:kotest-assertions-core:5.7.2'
testImplementation "io.mockk:mockk:1.13.7"
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutinesVersion")
}

// Allow references to generated code
Expand Down
44 changes: 34 additions & 10 deletions app/schemas/tech.relaycorp.letro.storage.LetroDatabase/1.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "d80c574c6882c1c80c0f103b6a6ec580",
"identityHash": "5f2174158254547dddd8483c68da34b4",
"entities": [
{
"tableName": "account",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `veraId` TEXT NOT NULL, `isCurrent` INTEGER NOT NULL, `isCreated` INTEGER NOT NULL)",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `veraidId` TEXT NOT NULL, `requestedUserName` TEXT NOT NULL, `normalisedLocale` TEXT NOT NULL, `isCurrent` INTEGER NOT NULL, `veraidPrivateKey` BLOB NOT NULL, `veraidMemberBundle` BLOB, `isCreated` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
Expand All @@ -15,8 +15,20 @@
"notNull": true
},
{
"fieldPath": "veraId",
"columnName": "veraId",
"fieldPath": "veraidId",
"columnName": "veraidId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "requestedUserName",
"columnName": "requestedUserName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "normalisedLocale",
"columnName": "normalisedLocale",
"affinity": "TEXT",
"notNull": true
},
Expand All @@ -26,6 +38,18 @@
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "veraidPrivateKey",
"columnName": "veraidPrivateKey",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "veraidMemberBundle",
"columnName": "veraidMemberBundle",
"affinity": "BLOB",
"notNull": false
},
{
"fieldPath": "isCreated",
"columnName": "isCreated",
Expand All @@ -41,20 +65,20 @@
},
"indices": [
{
"name": "index_account_veraId",
"name": "index_account_veraidId",
"unique": true,
"columnNames": [
"veraId"
"veraidId"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_account_veraId` ON `${TABLE_NAME}` (`veraId`)"
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_account_veraidId` ON `${TABLE_NAME}` (`veraidId`)"
}
],
"foreignKeys": []
},
{
"tableName": "contacts",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ownerVeraId` TEXT NOT NULL, `contactVeraId` TEXT NOT NULL, `alias` TEXT, `contactEndpointId` TEXT, `status` INTEGER NOT NULL, FOREIGN KEY(`ownerVeraId`) REFERENCES `account`(`veraId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ownerVeraId` TEXT NOT NULL, `contactVeraId` TEXT NOT NULL, `alias` TEXT, `contactEndpointId` TEXT, `status` INTEGER NOT NULL, FOREIGN KEY(`ownerVeraId`) REFERENCES `account`(`veraidId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
Expand Down Expand Up @@ -119,7 +143,7 @@
"ownerVeraId"
],
"referencedColumns": [
"veraId"
"veraidId"
]
}
]
Expand Down Expand Up @@ -262,7 +286,7 @@
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd80c574c6882c1c80c0f103b6a6ec580')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5f2174158254547dddd8483c68da34b4')"
]
}
}
44 changes: 41 additions & 3 deletions app/src/main/java/tech/relaycorp/letro/account/model/Account.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,50 @@ const val TABLE_NAME_ACCOUNT = "account"

@Entity(
tableName = TABLE_NAME_ACCOUNT,
indices = [Index("veraId", unique = true)],
indices = [Index("veraidId", unique = true)],
)
data class Account(
@PrimaryKey(autoGenerate = true)
val id: Long = 0L,
val veraId: String,
val veraidId: String,
val requestedUserName: String,
val normalisedLocale: String,
val isCurrent: Boolean,
// TODO: Encrypt key when integrating VeraId (https://relaycorp.atlassian.net/browse/LTR-55)
val veraidPrivateKey: ByteArray,
val veraidMemberBundle: ByteArray? = null,
val isCreated: Boolean = false,
)
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as Account

if (id != other.id) return false
if (veraidId != other.veraidId) return false
if (requestedUserName != other.requestedUserName) return false
if (normalisedLocale != other.normalisedLocale) return false
if (isCurrent != other.isCurrent) return false
if (!veraidPrivateKey.contentEquals(other.veraidPrivateKey)) return false
if (veraidMemberBundle != null) {
if (other.veraidMemberBundle == null) return false
if (!veraidMemberBundle.contentEquals(other.veraidMemberBundle)) return false
} else if (other.veraidMemberBundle != null) return false
if (isCreated != other.isCreated) return false

return true
}

override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + veraidId.hashCode()
result = 31 * result + requestedUserName.hashCode()
result = 31 * result + normalisedLocale.hashCode()
result = 31 * result + isCurrent.hashCode()
result = 31 * result + veraidPrivateKey.contentHashCode()
result = 31 * result + (veraidMemberBundle?.contentHashCode() ?: 0)
result = 31 * result + isCreated.hashCode()
return result
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ interface AccountDao {
@Update
suspend fun update(entity: Account): Int

@Query("SELECT * FROM $TABLE_NAME_ACCOUNT WHERE veraId=:veraId")
suspend fun getByVeraId(veraId: String): Account?
@Query("SELECT * FROM $TABLE_NAME_ACCOUNT WHERE id=:id")
suspend fun getById(id: Long): Account?

@Query("SELECT * FROM $TABLE_NAME_ACCOUNT WHERE requestedUserName=:requestedUserName AND normalisedLocale=:locale")
suspend fun getByRequestParams(requestedUserName: String, locale: String): Account?
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,26 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import tech.relaycorp.letro.account.model.Account
import tech.relaycorp.letro.main.MainViewModel
import tech.relaycorp.letro.utils.i18n.normaliseString
import java.security.PrivateKey
import java.util.Locale
import javax.inject.Inject

interface AccountRepository {
val currentAccount: Flow<Account?>
suspend fun createAccount(id: String)
suspend fun createAccount(
requestedUserName: String,
domainName: String,
locale: Locale,
veraidPrivateKey: PrivateKey,
)

suspend fun updateAccountId(id: String, newId: String)
suspend fun getByRequest(
requestedUserName: String,
locale: Locale,
): Account?

suspend fun updateAccount(account: Account, veraidId: String, veraidBundle: ByteArray)
}

class AccountRepositoryImpl @Inject constructor(
Expand Down Expand Up @@ -44,23 +57,40 @@ class AccountRepositoryImpl @Inject constructor(
}
}

override suspend fun createAccount(id: String) {
override suspend fun createAccount(
requestedUserName: String,
domainName: String,
locale: Locale,
veraidPrivateKey: PrivateKey,
) {
accountDao.insert(
Account(
veraId = id,
veraidId = "$requestedUserName@$domainName",
requestedUserName = requestedUserName,
normalisedLocale = locale.normaliseString(),
veraidPrivateKey = veraidPrivateKey.encoded,
isCurrent = true,
),
)
}

override suspend fun updateAccountId(id: String, newId: String) {
accountDao.getByVeraId(id)?.let {
accountDao.update(
it.copy(
veraId = newId,
isCreated = true,
),
)
}
override suspend fun getByRequest(requestedUserName: String, locale: Locale): Account? =
accountDao.getByRequestParams(
requestedUserName = requestedUserName,
locale = locale.normaliseString(),
)

override suspend fun updateAccount(
account: Account,
veraidId: String,
veraidBundle: ByteArray,
) {
accountDao.update(
account.copy(
veraidId = veraidId,
veraidMemberBundle = veraidBundle,
isCreated = true,
),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import android.util.Log
import tech.relaycorp.letro.awala.AwalaManagerImpl

sealed class MessageType(val value: String) {
object AccountCreationRequest : MessageType("application/vnd.relaycorp.letro.account-creation-request")
object AccountCreationCompleted : MessageType("application/vnd.relaycorp.letro.account-creation-completed-tmp")
object AccountCreationRequest : MessageType("application/vnd.relaycorp.letro.account-request")
object AccountCreation : MessageType("application/vnd.relaycorp.letro.account-creation")
object AuthorizeReceivingFromServer : MessageType("application/vnd+relaycorp.awala.pda-path")
object ContactPairingRequest : MessageType("application/vnd.relaycorp.letro.pairing-request-tmp")
object ContactPairingMatch : MessageType("application/vnd.relaycorp.letro.pairing-match-tmp")
Expand All @@ -18,7 +18,7 @@ sealed class MessageType(val value: String) {
fun from(type: String): MessageType {
return when (type) {
AccountCreationRequest.value -> AccountCreationRequest
AccountCreationCompleted.value -> AccountCreationCompleted
AccountCreation.value -> AccountCreation
AuthorizeReceivingFromServer.value -> AuthorizeReceivingFromServer
ContactPairingRequest.value -> ContactPairingRequest
ContactPairingMatch.value -> ContactPairingMatch
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ class ContactsViewModel @Inject constructor(
contactsCollectionJob = null
if (account != null) {
contactsCollectionJob = viewModelScope.launch {
contactsRepository.getContacts(account.veraId).collect {
contactsRepository.getContacts(account.veraidId).collect {
_contacts.emit(it.filter { it.status == ContactPairingStatus.COMPLETED })
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ class ManageContactViewModel @Inject constructor(
editingContact = contactToEdit
_uiState.update {
it.copy(
veraId = contactToEdit.contactVeraId,
veraidId = contactToEdit.contactVeraId,
alias = contactToEdit.alias,
isVeraIdInputEnabled = false,
)
Expand Down Expand Up @@ -107,7 +107,7 @@ class ManageContactViewModel @Inject constructor(
val trimmedId = id.trim()
_uiState.update {
it.copy(
veraId = trimmedId,
veraidId = trimmedId,
isSentRequestAgainHintVisible = contacts.any { it.contactVeraId == trimmedId && it.status == ContactPairingStatus.REQUEST_SENT },
)
}
Expand Down Expand Up @@ -140,7 +140,7 @@ class ManageContactViewModel @Inject constructor(
EDIT_CONTACT -> {
updateContact()
viewModelScope.launch {
_onEditContactCompleted.emit(uiState.value.veraId)
_onEditContactCompleted.emit(uiState.value.veraidId)
}
}
else -> throw IllegalStateException("Unknown screen type: $screenType")
Expand Down Expand Up @@ -195,7 +195,7 @@ class ManageContactViewModel @Inject constructor(
contactsRepository.addNewContact(
contact = Contact(
ownerVeraId = currentAccountId,
contactVeraId = uiState.value.veraId,
contactVeraId = uiState.value.veraidId,
alias = uiState.value.alias?.nullIfBlankOrEmpty(),
status = ContactPairingStatus.REQUEST_SENT,
),
Expand Down Expand Up @@ -230,7 +230,7 @@ class ManageContactViewModel @Inject constructor(

data class PairWithOthersUiState(
val manageContactTexts: ManageContactTexts,
val veraId: String = "",
val veraidId: String = "",
val alias: String? = null,
val isActionButtonEnabled: Boolean = false,
val isSentRequestAgainHintVisible: Boolean = false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const val TABLE_NAME_CONTACTS = "contacts"
foreignKeys = [
ForeignKey(
entity = Account::class,
parentColumns = ["veraId"],
parentColumns = ["veraidId"],
childColumns = ["ownerVeraId"],
onDelete = ForeignKey.CASCADE,
),
Expand Down
Loading

0 comments on commit 1f6eb3e

Please sign in to comment.