Skip to content

Commit

Permalink
Merge branch 'dev' of https://github.com/tonkeeper/android into featu…
Browse files Browse the repository at this point in the history
…re/cd
  • Loading branch information
polstianka committed Aug 6, 2024
2 parents cd2cf33 + 86c5baf commit 80b9cfa
Show file tree
Hide file tree
Showing 47 changed files with 1,361 additions and 710 deletions.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.tonapps.signer.screen.create

import android.content.Context
import android.util.Log
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
Expand All @@ -18,10 +17,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.single
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
Expand Down
30 changes: 20 additions & 10 deletions apps/wallet/api/src/main/java/com/tonapps/wallet/api/API.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.tonapps.wallet.api
import android.content.Context
import android.util.ArrayMap
import android.util.Log
import com.tonapps.blockchain.ton.extensions.EmptyPrivateKeyEd25519
import com.tonapps.blockchain.ton.extensions.base64
import com.tonapps.blockchain.ton.extensions.isValidTonAddress
import com.tonapps.extensions.locale
Expand Down Expand Up @@ -48,6 +49,7 @@ import org.json.JSONObject
import org.koin.androidx.viewmodel.lazyResolveViewModel
import org.ton.api.pub.PublicKeyEd25519
import org.ton.cell.Cell
import org.ton.crypto.hex
import java.util.Locale
import java.util.concurrent.TimeUnit

Expand Down Expand Up @@ -98,9 +100,11 @@ class API(

fun getAlertNotifications() = internalApi.getNotifications()

fun isOkStatus(testnet: Boolean): Boolean {
private suspend fun isOkStatus(testnet: Boolean): Boolean {
try {
val status = provider.blockchain.get(testnet).status()
val status = withRetry {
provider.blockchain.get(testnet).status()
} ?: return false
if (!status.restOnline) {
return false
}
Expand Down Expand Up @@ -232,13 +236,21 @@ class API(
}
}

fun getPublicKey(
suspend fun getPublicKey(
accountId: String,
testnet: Boolean
): String {
return accounts(testnet).getAccountPublicKey(accountId).publicKey
): PublicKeyEd25519? {
val hex = withRetry {
accounts(testnet).getAccountPublicKey(accountId)
}?.publicKey ?: return null
return PublicKeyEd25519(hex(hex))
}

suspend fun safeGetPublicKey(
accountId: String,
testnet: Boolean
) = getPublicKey(accountId, testnet) ?: EmptyPrivateKeyEd25519.publicKey()

fun accountEvents(accountId: String, testnet: Boolean): Flow<SSEvent> {
val endpoint = if (testnet) {
config.tonapiTestnetHost
Expand Down Expand Up @@ -336,13 +348,11 @@ class API(
if (!isOkStatus(testnet)) {
return@withContext false
}
try {
val request = SendBlockchainMessageRequest(boc)
val request = SendBlockchainMessageRequest(boc)
withRetry {
blockchain(testnet).sendBlockchainMessage(request)
true
} catch (e: Throwable) {
false
}
} ?: false
}

suspend fun sendToBlockchain(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ data class WalletEntity(
val hasPrivateKey: Boolean
get() = type == Wallet.Type.Default || type == Wallet.Type.Testnet || type == Wallet.Type.Lockup

val isSigner: Boolean
get() = type == Wallet.Type.Signer || type == Wallet.Type.SignerQR

val accountId: String = contract.address.toAccountId()

val address: String = contract.address.toWalletAddress(testnet)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ data class NftEntity(
val isDomain: Boolean
get() = dns != null

val ownerAddress: String
get() = owner?.address ?: address

val thumbUri: Uri by lazy {
getImageUri(64, 320) ?: previews.first().url.let { Uri.parse(it) }
}
Expand Down
1 change: 1 addition & 0 deletions apps/wallet/data/events/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,5 @@ dependencies {
implementation(project(Dependence.Lib.blockchain))
implementation(project(Dependence.Lib.extensions))
implementation(project(Dependence.Lib.icu))
implementation(project(Dependence.Lib.security))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
package com.tonapps.wallet.data.events

import com.tonapps.security.AesCbcState
import com.tonapps.security.Security
import io.ktor.util.hex
import org.ton.api.pk.PrivateKeyEd25519
import org.ton.api.pub.PublicKeyEd25519
import org.ton.block.AddrStd
import org.ton.cell.Cell
import org.ton.cell.CellBuilder
import org.ton.crypto.Ed25519
import kotlin.experimental.xor
import kotlin.math.ceil

object CommentEncryption {
private const val ROOT_CELL_BYTE_LENGTH = 35 + 4
private const val CELL_BYTE_LENGTH = 127

fun decryptComment(
publicKey: PublicKeyEd25519,
privateKey: PrivateKeyEd25519,
cipherText: String,
senderAddress: String
): String {
val address = AddrStd(senderAddress).toString(bounceable = true, urlSafe = true)

val decryptedData = decryptData(
hex(cipherText),
publicKey.key.toByteArray(),
privateKey.key.toByteArray(),
address.toByteArray()
)

return decryptedData.decodeToString()
}
fun encryptComment(
comment: String,
myPublicKey: PublicKeyEd25519,
theirPublicKey: PublicKeyEd25519,
myPrivateKey: PrivateKeyEd25519,
senderAddress: String
): Cell {
val myPublicKeyBytes = myPublicKey.key.toByteArray()
val theirPublicKeyBytes = theirPublicKey.key.toByteArray()
val myPrivateKeyBytes = myPrivateKey.key.toByteArray()
return encryptComment(comment, myPublicKeyBytes, theirPublicKeyBytes, myPrivateKeyBytes, senderAddress)
}

fun encryptComment(
comment: String,
myPublicKey: ByteArray,
theirPublicKey: ByteArray,
myPrivateKey: ByteArray,
senderAddress: String
): Cell {
if (comment.isEmpty()) throw IllegalArgumentException("empty comment")

val privateKey = if (myPrivateKey.size == 64) {
myPrivateKey.sliceArray(0 until 32)
} else {
myPrivateKey
}

val commentBytes = comment.toByteArray()

val address = AddrStd(senderAddress).toString(bounceable = true, urlSafe = true)
val salt = address.toByteArray()

val encryptedBytes =
encryptData(commentBytes, myPublicKey, theirPublicKey, privateKey, salt)

val payload = ByteArray(encryptedBytes.size + 4)
payload[0] = 0x21 // encrypted text prefix
payload[1] = 0x67
payload[2] = 0xda.toByte()
payload[3] = 0x4b
encryptedBytes.copyInto(payload, 4)

return makeSnakeCells(payload)
}

private fun makeSnakeCells(bytes: ByteArray): Cell {
val rootCellBuilder = CellBuilder.beginCell()
.storeBytes(bytes.sliceArray(0 until minOf(bytes.size, ROOT_CELL_BYTE_LENGTH)))

val cellCount =
ceil((bytes.size - ROOT_CELL_BYTE_LENGTH).toDouble() / CELL_BYTE_LENGTH).toInt()
if (cellCount > 16) {
throw IllegalArgumentException("Text too long")
}

rootCellBuilder.storeRef(storeDeepRef(bytes.sliceArray(ROOT_CELL_BYTE_LENGTH until bytes.size)))

return rootCellBuilder.endCell()
}

private fun storeDeepRef(bytes: ByteArray): Cell {
return if (bytes.size <= CELL_BYTE_LENGTH) {
CellBuilder.beginCell().storeBytes(bytes.sliceArray(0 until bytes.size)).endCell()
} else {
CellBuilder.beginCell().storeBytes(bytes.sliceArray(0 until CELL_BYTE_LENGTH))
.storeRef(storeDeepRef(bytes.sliceArray(CELL_BYTE_LENGTH until bytes.size)))
.endCell()
}
}

private fun encryptData(
data: ByteArray,
myPublicKey: ByteArray,
theirPublicKey: ByteArray,
privateKey: ByteArray,
salt: ByteArray
): ByteArray {
val sharedSecret = Ed25519.sharedKey(privateKey, theirPublicKey)

val encrypted = encryptDataImpl(data, sharedSecret, salt)
val prefixedEncrypted = ByteArray(myPublicKey.size + encrypted.size)

for (i in myPublicKey.indices) {
prefixedEncrypted[i] = (theirPublicKey[i] xor myPublicKey[i]).toByte()
}

encrypted.copyInto(prefixedEncrypted, myPublicKey.size)

return prefixedEncrypted
}

private fun encryptDataImpl(
data: ByteArray,
sharedSecret: ByteArray,
salt: ByteArray
): ByteArray {
val prefix = getRandomPrefix(data.size, 16)
val combined = ByteArray(prefix.size + data.size)
System.arraycopy(prefix, 0, combined, 0, prefix.size)
System.arraycopy(data, 0, combined, prefix.size, data.size)
return encryptDataWithPrefix(combined, sharedSecret, salt)
}

private fun encryptDataWithPrefix(
data: ByteArray,
sharedSecret: ByteArray,
salt: ByteArray
): ByteArray {
if (data.size % 16 != 0) throw IllegalArgumentException("Data length is not divisible by 16")
val dataHash = Security.hmacSha512(salt, data)
val msgKey = dataHash.sliceArray(0 until 16)
val cbcStateSecret = Security.hmacSha512(sharedSecret, msgKey)
val encrypted = AesCbcState(cbcStateSecret).encrypt(data)
return msgKey + encrypted
}

private fun getRandomPrefix(dataLength: Int, minPadding: Int): ByteArray {
val prefixLength = ((minPadding + 15 + dataLength) and -16) - dataLength
val prefix = Security.randomBytes(prefixLength)
prefix[0] = prefixLength.toByte()
if ((prefixLength + dataLength) % 16 != 0) throw IllegalArgumentException("Prefix length is invalid")
return prefix
}

private fun decryptData(
data: ByteArray,
publicKey: ByteArray,
privateKey: ByteArray,
salt: ByteArray
): ByteArray {
val theirPublicKey = ByteArray(publicKey.size)
for (i in publicKey.indices) {
theirPublicKey[i] = data[i] xor publicKey[i]
}
val sharedSecret = Ed25519.sharedKey(privateKey, theirPublicKey)
return decryptDataImpl(data.sliceArray(publicKey.size until data.size), sharedSecret, salt)
}

private fun decryptDataImpl(
encryptedData: ByteArray,
sharedSecret: ByteArray,
salt: ByteArray
): ByteArray {
if (encryptedData.size < 16) throw IllegalArgumentException("Failed to decrypt: data is too small")
if (encryptedData.size % 16 != 0) throw IllegalArgumentException("Failed to decrypt: data size is not divisible by 16")

val msgKey = encryptedData.sliceArray(0 until 16)
val data = encryptedData.sliceArray(16 until encryptedData.size)
val cbcStateSecret = Security.hmacSha512(sharedSecret, msgKey)

return doDecrypt(cbcStateSecret, msgKey, data, salt)
}

private fun doDecrypt(
cbcStateSecret: ByteArray,
msgKey: ByteArray,
encryptedData: ByteArray,
salt: ByteArray
): ByteArray {
val decryptedData = AesCbcState(cbcStateSecret).decrypt(encryptedData)
val dataHash = Security.hmacSha512(salt, decryptedData)

val gotMsgKey = dataHash.sliceArray(0 until 16)

if (!msgKey.contentEquals(gotMsgKey)) {
throw IllegalArgumentException("Failed to decrypt: hash mismatch")
}

val prefixLength = decryptedData[0].toInt()
if (prefixLength > decryptedData.size || prefixLength < 16) {
throw IllegalArgumentException("Failed to decrypt: invalid prefix size")
}

return decryptedData.sliceArray(prefixLength until decryptedData.size)
}


}
Loading

0 comments on commit 80b9cfa

Please sign in to comment.