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

Opt/spend key #5116

Merged
merged 17 commits into from
Nov 26, 2024
Binary file modified app/libs/mixin.aar
Binary file not shown.
2 changes: 1 addition & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
android:launchMode="singleTask"
android:theme="@style/AppTheme.NoAnim"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".ui.landing.InitializeActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/java/one/mixin/android/MixinApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,16 @@ open class MixinApplication :
}
}

fun reject() {
MixinDatabase.destroy()
val entryPoint =
EntryPointAccessors.fromApplication(
this@MixinApplication,
AppEntryPoint::class.java,
)
entryPoint.inject(this@MixinApplication)
}

private fun clearData(sessionId: String?) {
val jobManager = getJobManager()
jobManager.cancelAllJob()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import java.util.zip.CRC32

fun isMnemonicValid(words: List<String>): Boolean {
val nativeResult = runCatching {
MnemonicCode.toSeed(words, "")
MnemonicCode.INSTANCE.check(words)
}.getOrNull() != null
require(Blockchain.isMnemonicValid(words.joinToString(" ")) == nativeResult)
return nativeResult
Expand Down Expand Up @@ -85,4 +85,11 @@ fun mnemonicChecksumWord(words: List<String>, prefixLen: Int = 3): String {
val word = MnemonicCode.INSTANCE.wordList[(checksum % MnemonicCode.INSTANCE.wordList.size).toInt()]
require(word == Blockchain.mnemonicChecksumWord(words.joinToString(" "), 3))
return word
}

fun getMatchingWords(input: String): List<String>? {
if (MnemonicCode.INSTANCE.wordList.contains(input)) {
return null
}
return MnemonicCode.INSTANCE.wordList.filter { it.startsWith(input) }
}
4 changes: 4 additions & 0 deletions app/src/main/java/one/mixin/android/db/MixinDatabase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,10 @@ abstract class MixinDatabase : RoomDatabase() {
private val lock = Any()
private var supportSQLiteDatabase: SupportSQLiteDatabase? = null

fun destroy() {
INSTANCE = null
}

@Suppress("UNUSED_ANONYMOUS_PARAMETER")
@SuppressLint("RestrictedApi")
fun getDatabase(context: Context): MixinDatabase {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,9 @@ class ConversationRepository

fun observeConversations(circleId: String?): DataSource.Factory<Int, ConversationItem> =
if (circleId == null) {
DataProvider.observeConversations(appDatabase)
DataProvider.observeConversations(MixinDatabase.getDatabase(MixinApplication.appContext))
} else {
DataProvider.observeConversationsByCircleId(circleId, appDatabase)
DataProvider.observeConversationsByCircleId(circleId, MixinDatabase.getDatabase(MixinApplication.appContext))
}

suspend fun successConversationList(): List<ConversationMinimal> =
Expand Down
46 changes: 21 additions & 25 deletions app/src/main/java/one/mixin/android/tip/Tip.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ import one.mixin.android.crypto.aesEncrypt
import one.mixin.android.crypto.argon2IHash
import one.mixin.android.crypto.generateRandomBytes
import one.mixin.android.crypto.getValueFromEncryptedPreferences
import one.mixin.android.crypto.isMnemonicValid
import one.mixin.android.crypto.newKeyPairFromMnemonic
import one.mixin.android.crypto.newKeyPairFromSeed
import one.mixin.android.crypto.sha3Sum256
import one.mixin.android.crypto.storeValueInEncryptedPreferences
import one.mixin.android.crypto.toCompleteMnemonic
import one.mixin.android.crypto.toMnemonic
import one.mixin.android.crypto.toSeed
import one.mixin.android.event.TipEvent
import one.mixin.android.extension.base64RawURLDecode
import one.mixin.android.extension.base64RawURLEncode
Expand All @@ -49,8 +49,6 @@ import one.mixin.android.tip.exception.TipNotAllWatcherSuccessException
import one.mixin.android.tip.exception.TipNullException
import one.mixin.android.util.ErrorHandler
import one.mixin.android.util.reportException
import org.bitcoinj.crypto.DeterministicKey
import org.bitcoinj.crypto.HDKeyDerivation.createMasterPrivateKey
import timber.log.Timber
import java.io.IOException
import javax.inject.Inject
Expand Down Expand Up @@ -169,15 +167,6 @@ class Tip
}
}

fun getMasterKeyFromMnemonic(context: Context): DeterministicKey {
var entropy = getMnemonicFromEncryptedPreferences(context)
if (entropy == null) { // Register safe must generate mnemonic
entropy = generateEntropyAndStore(context)
}
val seed = toSeed(toMnemonic(entropy).split(" "), "")
return createMasterPrivateKey(seed)
}

suspend fun checkSalt(context: Context, pin: String, tipPriv: ByteArray) {
if (!Session.hasPhone()){
val saltAESKey = generateSaltAESKey(pin, tipPriv)
Expand All @@ -190,7 +179,7 @@ class Tip
}

suspend fun getEncryptSalt(context: Context, pin: String, tipPriv: ByteArray, force: Boolean = false): String {
val salt = if (Session.hasPhone() || force) {
val rawSalt = if (Session.hasPhone() || force) {
var local = getMnemonicFromEncryptedPreferences(context)
if (local == null) {
val saltAESKey = generateSaltAESKey(pin, tipPriv)
Expand All @@ -202,7 +191,7 @@ class Tip
ByteArray(16)
}
val saltAESKey = generateSaltAESKey(pin, tipPriv)
val encryptedSalt = aesEncrypt(saltAESKey, salt)
val encryptedSalt = aesEncrypt(saltAESKey, rawSalt)
val pinToken = Session.getPinToken()?.decodeBase64() ?: throw TipNullException("No pin token")
return aesEncrypt(pinToken, encryptedSalt).base64RawURLEncode()
}
Expand All @@ -214,14 +203,14 @@ class Tip
do {
entropy = generateRandomBytes(16)
mnemonicPhrase = toCompleteMnemonic(toMnemonic(entropy))
} while (mnemonicPhrase.distinct().size != mnemonicPhrase.size)
} while (mnemonicPhrase.distinct().size != mnemonicPhrase.size && isMnemonicValid(mnemonicPhrase))
storeValueInEncryptedPreferences(context, Constants.Tip.MNEMONIC, entropy)
return entropy
}

suspend fun getMnemonicEdKey(context: Context, pin: String, tipPriv: ByteArray): EdKeyPair {
var entropy = getMnemonicFromEncryptedPreferences(context)
if (entropy == null) {
if (entropy == null) { // If not exist, get it from safe and decrypt it
val saltAESKey = generateSaltAESKey(pin, tipPriv)
val encryptedSalt = getEncryptedSalt(context)
entropy = aesDecrypt(saltAESKey, encryptedSalt)
Expand All @@ -230,12 +219,6 @@ class Tip
return edKey
}

fun getMnemonicEdKey(context: Context): EdKeyPair {
val entropy = getMnemonicFromEncryptedPreferences(context) ?: throw NullPointerException()
val edKey = newKeyPairFromMnemonic(toMnemonic(entropy))
return edKey
}

suspend fun getMnemonicOrFetchFromSafe(context: Context, pin: String): List<String>? {
val entropy = getMnemonicFromEncryptedPreferences(context)
if (entropy != null) {
Expand Down Expand Up @@ -264,14 +247,19 @@ class Tip
}

fun getSpendPrivFromEncryptedSalt(
entropy: ByteArray?,
encryptedSalt: ByteArray,
pin: String,
tipPriv: ByteArray,
): ByteArray {
): ByteArray {
if (entropy == null) {
val saltAESKey = generateSaltAESKey(pin, tipPriv)
val salt = aesDecrypt(saltAESKey, encryptedSalt)
return getSpendPriv(tipPriv, salt)
} else {
return getSpendPriv(tipPriv, entropy)
}
}

private fun getSalt(
encryptedSalt: ByteArray,
Expand All @@ -283,7 +271,15 @@ class Tip
return salt
}

fun getSpendPriv(
fun getSpendPriv(context: Context, seed: ByteArray): ByteArray {
var entropy = getMnemonicFromEncryptedPreferences(context)
if (entropy == null) { // Register safe must generate mnemonic, Only once
entropy = generateEntropyAndStore(context)
}
return getSpendPriv(seed, entropy)
}

private fun getSpendPriv(
tipPriv: ByteArray,
salt: ByteArray,
): ByteArray =
Expand Down Expand Up @@ -683,7 +679,7 @@ class Tip
deleteKeyByAlias(Constants.Tip.ALIAS_TIP_PRIV)
}

private fun getMnemonicFromEncryptedPreferences(context: Context): ByteArray? {
fun getMnemonicFromEncryptedPreferences(context: Context): ByteArray? {
return getValueFromEncryptedPreferences(context, Constants.Tip.MNEMONIC)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package one.mixin.android.ui.common
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.protobuf.Mixin
import dagger.hilt.android.lifecycle.HiltViewModel
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
Expand Down Expand Up @@ -202,8 +203,9 @@ class BottomSheetViewModel
null
}

val tipPriv = tip.getOrRecoverTipPriv(MixinApplication.appContext, pin).getOrThrow()
val spendKey = tip.getSpendPrivFromEncryptedSalt(tip.getEncryptedSalt(MixinApplication.appContext), pin, tipPriv)
val context = MixinApplication.appContext
val tipPriv = tip.getOrRecoverTipPriv(context, pin).getOrThrow()
val spendKey = tip.getSpendPrivFromEncryptedSalt(tip.getMnemonicFromEncryptedPreferences(context), tip.getEncryptedSalt(context), pin, tipPriv)
Timber.e("Kernel Withdrawal($traceId): begin")
Timber.e("Kernel Withdrawal($traceId): request ghost key")
val ghostKeyResponse =
Expand Down Expand Up @@ -375,8 +377,9 @@ class BottomSheetViewModel
reference: String?,
): MixinResponse<*> {
val asset = assetIdToAsset(assetId)
val tipPriv = tip.getOrRecoverTipPriv(MixinApplication.appContext, pin).getOrThrow()
val spendKey = tip.getSpendPrivFromEncryptedSalt(tip.getEncryptedSalt(MixinApplication.appContext), pin, tipPriv)
val context = MixinApplication.appContext
val tipPriv = tip.getOrRecoverTipPriv(context, pin).getOrThrow()
val spendKey = tip.getSpendPrivFromEncryptedSalt(tip.getMnemonicFromEncryptedPreferences(context), tip.getEncryptedSalt(context), pin, tipPriv)
val utxoWrapper = UtxoWrapper(packUtxo(asset, amount))

Timber.e("Kernel Address Transaction($trace): begin")
Expand Down Expand Up @@ -455,10 +458,11 @@ class BottomSheetViewModel
inscriptionHash: String? = null,
release: Boolean? = null,
): MixinResponse<*> {
val context = MixinApplication.appContext
val isConsolidation = receiverIds.size == 1 && receiverIds.first() == Session.getAccountId()
val asset = assetIdToAsset(assetId)
val tipPriv = tip.getOrRecoverTipPriv(MixinApplication.appContext, pin).getOrThrow()
val spendKey = tip.getSpendPrivFromEncryptedSalt(tip.getEncryptedSalt(MixinApplication.appContext), pin, tipPriv)
val tipPriv = tip.getOrRecoverTipPriv(context, pin).getOrThrow()
val spendKey = tip.getSpendPrivFromEncryptedSalt(tip.getMnemonicFromEncryptedPreferences(context), tip.getEncryptedSalt(context), pin, tipPriv)
val utxoWrapper = UtxoWrapper(packUtxo(asset, amount, inscriptionHash))
Timber.e("Kernel Transaction($trace): begin")
val rawTransaction = tokenRepository.findRawTransaction(trace)
Expand Down Expand Up @@ -1306,9 +1310,10 @@ class BottomSheetViewModel
t: SafeMultisigsBiometricItem,
pin: String,
): MixinResponse<TransactionResponse> {
val tipPriv = tip.getOrRecoverTipPriv(MixinApplication.appContext, pin).getOrThrow()
val context = MixinApplication.appContext
val tipPriv = tip.getOrRecoverTipPriv(context, pin).getOrThrow()
return if (t.action == "sign") {
val spendKey = tip.getSpendPrivFromEncryptedSalt(tip.getEncryptedSalt(MixinApplication.appContext), pin, tipPriv)
val spendKey = tip.getSpendPrivFromEncryptedSalt(tip.getMnemonicFromEncryptedPreferences(context), tip.getEncryptedSalt(context), pin, tipPriv)
val sign = Kernel.signTransaction(t.raw, t.views, spendKey.toHex(), t.index.toLong(), false)
tokenRepository.signTransactionMultisigs(t.traceId, TransactionRequest(sign.raw, t.traceId))
} else if (t.action == "unlock") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ abstract class PinCodeFragment(
RestoreActivity.show(requireContext())
}
}
MixinApplication.get().reject()
activity?.finish()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,8 @@ class ConversationListFragment : LinkFragment() {
if (isAdded) {
messageAdapter.unregisterAdapterDataObserver(messageAdapterDataObserver)
}
conversationLiveData?.removeObserver(observer)
dotLiveData?.removeObserver(dotObserver)
super.onDestroyView()
_binding = null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class BrowserWalletBottomSheetViewModel
chainId: String,
): ByteArray {
val result = tip.getOrRecoverTipPriv(context, pin)
val spendKey = tip.getSpendPrivFromEncryptedSalt(tip.getEncryptedSalt(context), pin, result.getOrThrow())
val spendKey = tip.getSpendPrivFromEncryptedSalt(tip.getMnemonicFromEncryptedPreferences(context), tip.getEncryptedSalt(context), pin, result.getOrThrow())
return tipPrivToPrivateKey(spendKey, chainId)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,14 @@ package one.mixin.android.ui.landing
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import one.mixin.android.Constants
import one.mixin.android.R
import one.mixin.android.databinding.FragmentComposeBinding
import one.mixin.android.extension.addFragment
import one.mixin.android.extension.openUrl
import one.mixin.android.ui.landing.MobileFragment.Companion.FROM_LANDING
import one.mixin.android.ui.landing.MobileFragment.Companion.FROM_LANDING_CREATE
import one.mixin.android.ui.landing.components.CreateAccountPage
import one.mixin.android.util.viewBinding
import one.mixin.android.vo.Hyperlink

class CreateAccountFragment : Fragment(R.layout.fragment_compose) {
companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import one.mixin.android.Constants.Account.PREF_LOGIN_VERIFY
import one.mixin.android.Constants.Account.PREF_TRIED_UPDATE_KEY
import one.mixin.android.Constants.DEVICE_ID
import one.mixin.android.Constants.TEAM_BOT_ID
import one.mixin.android.Constants.TEAM_BOT_NAME
import one.mixin.android.Constants.TEAM_MIXIN_USER_ID
Expand All @@ -28,6 +29,7 @@ import one.mixin.android.databinding.FragmentLoadingBinding
import one.mixin.android.extension.base64Encode
import one.mixin.android.extension.decodeBase64
import one.mixin.android.extension.defaultSharedPreferences
import one.mixin.android.extension.getStringDeviceId
import one.mixin.android.extension.putBoolean
import one.mixin.android.extension.viewDestroyed
import one.mixin.android.job.InitializeJob
Expand All @@ -36,6 +38,10 @@ import one.mixin.android.session.Session
import one.mixin.android.session.decryptPinToken
import one.mixin.android.ui.common.BaseFragment
import one.mixin.android.ui.home.MainActivity
import one.mixin.android.ui.tip.TipActivity
import one.mixin.android.ui.tip.TipBundle
import one.mixin.android.ui.tip.TipType
import one.mixin.android.ui.tip.TryConnecting
import one.mixin.android.util.ErrorHandler
import one.mixin.android.util.ErrorHandler.Companion.FORBIDDEN
import one.mixin.android.util.reportException
Expand Down Expand Up @@ -103,8 +109,14 @@ class LoadingFragment : BaseFragment(R.layout.fragment_loading) {

if (Session.hasSafe()) {
defaultSharedPreferences.putBoolean(PREF_LOGIN_VERIFY, true)
MainActivity.show(requireContext())
} else {
var deviceId = defaultSharedPreferences.getString(DEVICE_ID, null)
if (deviceId == null) {
deviceId = requireActivity().getStringDeviceId()
}
TipActivity.show(requireActivity(), TipBundle(TipType.Create, deviceId, TryConnecting, null))
}
MainActivity.show(requireContext())
activity?.finish()
}

Expand Down
Loading