Profile toolbar & 4 ChatItem types & Other UI improvements

This commit is contained in:
Matéo Duparc 2021-05-21 14:55:37 +02:00
parent e77887a51c
commit 2f01c86359
Signed by: hardcoresushi
GPG Key ID: 007F84120107191E
18 changed files with 308 additions and 225 deletions

View File

@ -42,9 +42,9 @@ android {
dependencies { dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.core:core-ktx:1.5.0'
implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.appcompat:appcompat:1.3.0'
implementation "androidx.fragment:fragment-ktx:1.3.3" implementation "androidx.fragment:fragment-ktx:1.3.4"
implementation "androidx.preference:preference-ktx:1.1.1" implementation "androidx.preference:preference-ktx:1.1.1"
implementation 'com.google.android.material:material:1.3.0' implementation 'com.google.android.material:material:1.3.0'
//implementation 'androidx.constraintlayout:constraintlayout:2.0.4' //implementation 'androidx.constraintlayout:constraintlayout:2.0.4'

View File

@ -28,8 +28,8 @@
</intent-filter> </intent-filter>
</receiver> </receiver>
<activity android:name=".ChatActivity"/> <activity android:name=".ChatActivity" android:theme="@style/Theme.AIRA.NoActionBar"/>
<activity android:name=".MainActivity"> <activity android:name=".MainActivity" android:theme="@style/Theme.AIRA.NoActionBar">
<intent-filter android:label="@string/share_label"> <intent-filter android:label="@string/share_label">
<action android:name="android.intent.action.SEND"/> <action android:name="android.intent.action.SEND"/>
<action android:name="android.intent.action.SEND_MULTIPLE"/> <action android:name="android.intent.action.SEND_MULTIPLE"/>

View File

@ -1,16 +1,17 @@
package sushi.hardcore.aira package sushi.hardcore.aira
import android.content.ComponentName import android.content.ComponentName
import android.content.Context
import android.content.ServiceConnection import android.content.ServiceConnection
import android.os.Bundle import android.os.Bundle
import android.os.IBinder import android.os.IBinder
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -32,14 +33,7 @@ class ChatActivity : ServiceBoundActivity() {
private var lastLoadedMessageOffset = 0 private var lastLoadedMessageOffset = 0
private val filePicker = registerForActivityResult(ActivityResultContracts.GetMultipleContents()) { uris -> private val filePicker = registerForActivityResult(ActivityResultContracts.GetMultipleContents()) { uris ->
if (isServiceInitialized() && uris.size > 0) { if (isServiceInitialized() && uris.size > 0) {
airaService.sendFilesFromUris(sessionId, uris)?.let { msgs -> airaService.sendFilesFromUris(sessionId, uris)
for (msg in msgs) {
chatAdapter.newMessage(ChatItem(true, msg))
if (airaService.contacts.contains(sessionId)) {
lastLoadedMessageOffset += 1
}
}
}
} }
} }
@ -47,13 +41,15 @@ class ChatActivity : ServiceBoundActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityChatBinding.inflate(layoutInflater) binding = ActivityChatBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
sessionId = intent.getIntExtra("sessionId", -1) sessionId = intent.getIntExtra("sessionId", -1)
if (sessionId != -1) { if (sessionId != -1) {
intent.getStringExtra("sessionName")?.let { name -> intent.getStringExtra("sessionName")?.let { name ->
sessionName = name sessionName = name
title = name setSupportActionBar(binding.toolbar.toolbar)
binding.toolbar.textAvatar.setLetterFrom(name)
binding.toolbar.title.text = name
supportActionBar?.setDisplayHomeAsUpEnabled(true)
chatAdapter = ChatAdapter(this@ChatActivity, ::onClickSaveFile) chatAdapter = ChatAdapter(this@ChatActivity, ::onClickSaveFile)
binding.recyclerChat.apply { binding.recyclerChat.apply {
@ -112,7 +108,7 @@ class ChatActivity : ServiceBoundActivity() {
} }
binding.recyclerChat.smoothScrollToPosition(chatAdapter.itemCount) binding.recyclerChat.smoothScrollToPosition(chatAdapter.itemCount)
val showBottomPanel = { val showBottomPanel = {
findViewById<ConstraintLayout>(R.id.bottom_panel).visibility = View.VISIBLE binding.bottomPanel.visibility = View.VISIBLE
} }
airaService.uiCallbacks = object : AIRAService.UiCallbacks { airaService.uiCallbacks = object : AIRAService.UiCallbacks {
override fun onNewSession(sessionId: Int, ip: String) { override fun onNewSession(sessionId: Int, ip: String) {
@ -125,7 +121,9 @@ class ChatActivity : ServiceBoundActivity() {
override fun onSessionDisconnect(sessionId: Int) { override fun onSessionDisconnect(sessionId: Int) {
if (this@ChatActivity.sessionId == sessionId) { if (this@ChatActivity.sessionId == sessionId) {
runOnUiThread { runOnUiThread {
findViewById<ConstraintLayout>(R.id.bottom_panel).visibility = View.GONE val inputManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputManager.hideSoftInputFromWindow(binding.editMessage.windowToken, 0)
binding.bottomPanel.visibility = View.GONE
} }
} }
} }

View File

@ -4,8 +4,14 @@ import sushi.hardcore.aira.background_service.Protocol
class ChatItem(val outgoing: Boolean, val data: ByteArray) { class ChatItem(val outgoing: Boolean, val data: ByteArray) {
companion object { companion object {
const val MESSAGE = 0 const val OUTGOING_MESSAGE = 0
const val FILE = 1 const val INCOMING_MESSAGE = 1
const val OUTGOING_FILE = 2
const val INCOMING_FILE = 3
}
val itemType = if (data[0] == Protocol.MESSAGE) {
if (outgoing) OUTGOING_MESSAGE else INCOMING_MESSAGE
} else {
if (outgoing) OUTGOING_FILE else INCOMING_FILE
} }
val itemType = if (data[0] == Protocol.MESSAGE) { MESSAGE } else { FILE }
} }

View File

@ -79,7 +79,11 @@ class MainActivity : ServiceBoundActivity() {
setContentView(binding.root) setContentView(binding.root)
val identityName = intent.getStringExtra("identityName") val identityName = intent.getStringExtra("identityName")
identityName?.let { title = it } identityName?.let {
setSupportActionBar(binding.toolbar.toolbar)
binding.toolbar.textAvatar.setLetterFrom(it)
binding.toolbar.title.text = it
}
val openedToShareFile = intent.action == Intent.ACTION_SEND || intent.action == Intent.ACTION_SEND_MULTIPLE val openedToShareFile = intent.action == Intent.ACTION_SEND || intent.action == Intent.ACTION_SEND_MULTIPLE

View File

@ -11,7 +11,6 @@ import android.widget.ImageButton
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.marginEnd
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import sushi.hardcore.aira.ChatItem import sushi.hardcore.aira.ChatItem
@ -23,7 +22,8 @@ class ChatAdapter(
): RecyclerView.Adapter<RecyclerView.ViewHolder>() { ): RecyclerView.Adapter<RecyclerView.ViewHolder>() {
companion object { companion object {
const val BUBBLE_MARGIN = 70 const val CONTAINER_MARGIN = 70
const val BUBBLE_HORIZONTAL_PADDING = 40
} }
private val inflater: LayoutInflater = LayoutInflater.from(context) private val inflater: LayoutInflater = LayoutInflater.from(context)
@ -44,88 +44,106 @@ class ChatAdapter(
notifyDataSetChanged() notifyDataSetChanged()
} }
internal open class BubbleViewHolder(private val context: Context, itemView: View): RecyclerView.ViewHolder(itemView) { internal open class BubbleViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
fun handleItemView(position: Int) { protected fun setPadding(outgoing: Boolean) {
itemView.updatePadding(top = if (position == 0) {
50
} else {
itemView.paddingBottom
})
}
fun setBubbleColor(bubble: View, outgoing: Boolean) {
if (outgoing) { if (outgoing) {
bubble.background.clearColorFilter() itemView.updatePadding(right = BUBBLE_HORIZONTAL_PADDING)
} else { } else {
bubble.background.colorFilter = PorterDuffColorFilter(ContextCompat.getColor(context, R.color.incomingBubbleBackground), PorterDuff.Mode.SRC) itemView.updatePadding(left = BUBBLE_HORIZONTAL_PADDING)
}
}
protected fun configureBubble(context: Context, view: View, outgoing: Boolean) {
view.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply {
gravity = if (outgoing) {
marginStart = CONTAINER_MARGIN
Gravity.END
} else {
marginEnd = CONTAINER_MARGIN
Gravity.START
}
}
if (!outgoing) {
view.background.colorFilter = PorterDuffColorFilter(
ContextCompat.getColor(context, R.color.incomingBubbleBackground),
PorterDuff.Mode.SRC
)
} }
} }
} }
internal class MessageViewHolder(context: Context, itemView: View): BubbleViewHolder(context, itemView) { internal open class MessageViewHolder(itemView: View): BubbleViewHolder(itemView) {
fun bind(chatItem: ChatItem, position: Int) { protected fun bindMessage(chatItem: ChatItem): TextView {
itemView.findViewById<TextView>(R.id.text_message).apply { itemView.findViewById<TextView>(R.id.text_message).apply {
text = chatItem.data.sliceArray(1 until chatItem.data.size).decodeToString() text = chatItem.data.sliceArray(1 until chatItem.data.size).decodeToString()
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { return this
if (chatItem.outgoing) {
gravity = Gravity.END
marginStart = BUBBLE_MARGIN
} else {
gravity = Gravity.START
marginEnd = BUBBLE_MARGIN
}
}
setBubbleColor(this, chatItem.outgoing)
} }
handleItemView(position)
} }
} }
internal class FileViewHolder(context: Context, itemView: View): BubbleViewHolder(context, itemView) { internal class OutgoingMessageViewHolder(private val context: Context, itemView: View): MessageViewHolder(itemView) {
fun bind(chatItem: ChatItem, position: Int, onSavingFile: (filename: String, rawUuid: ByteArray) -> Unit) { fun bind(chatItem: ChatItem) {
configureBubble(context, bindMessage(chatItem), true)
setPadding(true)
}
}
internal class IncomingMessageViewHolder(private val context: Context, itemView: View): MessageViewHolder(itemView) {
fun bind(chatItem: ChatItem) {
configureBubble(context, bindMessage(chatItem), false)
setPadding(false)
}
}
internal open class FileViewHolder(itemView: View, private val onSavingFile: (filename: String, rawUuid: ByteArray) -> Unit): BubbleViewHolder(itemView) {
protected fun bindFile(chatItem: ChatItem): LinearLayout {
val filename = chatItem.data.sliceArray(17 until chatItem.data.size).decodeToString() val filename = chatItem.data.sliceArray(17 until chatItem.data.size).decodeToString()
itemView.findViewById<TextView>(R.id.text_filename).text = filename itemView.findViewById<TextView>(R.id.text_filename).text = filename
itemView.findViewById<ImageButton>(R.id.button_save).setOnClickListener { itemView.findViewById<ImageButton>(R.id.button_save).setOnClickListener {
onSavingFile(filename, chatItem.data.sliceArray(1 until 17)) onSavingFile(filename, chatItem.data.sliceArray(1 until 17))
} }
itemView.findViewById<LinearLayout>(R.id.bubble_content).apply { return itemView.findViewById(R.id.bubble_content)
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { }
if (chatItem.outgoing) { }
gravity = Gravity.END
marginStart = BUBBLE_MARGIN internal class OutgoingFileViewHolder(private val context: Context, itemView: View, onSavingFile: (filename: String, rawUuid: ByteArray) -> Unit): FileViewHolder(itemView, onSavingFile) {
} else { fun bind(chatItem: ChatItem) {
gravity = Gravity.START configureBubble(context, bindFile(chatItem), true)
marginEnd = BUBBLE_MARGIN setPadding(true)
} }
} }
setBubbleColor(this, chatItem.outgoing)
} internal class IncomingFileViewHolder(private val context: Context, itemView: View, onSavingFile: (filename: String, rawUuid: ByteArray) -> Unit): FileViewHolder(itemView, onSavingFile) {
handleItemView(position) fun bind(chatItem: ChatItem) {
configureBubble(context, bindFile(chatItem), false)
setPadding(false)
} }
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) { return if (viewType == ChatItem.OUTGOING_MESSAGE || viewType == ChatItem.INCOMING_MESSAGE) {
ChatItem.MESSAGE -> { val view = inflater.inflate(R.layout.adapter_chat_message, parent, false)
val view = inflater.inflate(R.layout.adapter_chat_message, parent, false) if (viewType == ChatItem.OUTGOING_MESSAGE) {
MessageViewHolder(context, view) OutgoingMessageViewHolder(context, view)
} else {
IncomingMessageViewHolder(context, view)
} }
ChatItem.FILE -> { } else {
val view = inflater.inflate(R.layout.adapter_chat_file, parent, false) val view = inflater.inflate(R.layout.adapter_chat_file, parent, false)
FileViewHolder(context, view) if (viewType == ChatItem.OUTGOING_FILE) {
OutgoingFileViewHolder(context, view, onSavingFile)
} else {
IncomingFileViewHolder(context, view, onSavingFile)
} }
else -> throw RuntimeException("Invalid chat item type")
} }
} }
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val chatItem = chatItems[position] val chatItem = chatItems[position]
when (chatItem.itemType) { when (chatItem.itemType) {
ChatItem.MESSAGE -> { ChatItem.OUTGOING_MESSAGE -> (holder as OutgoingMessageViewHolder).bind(chatItem)
(holder as MessageViewHolder).bind(chatItem, position) ChatItem.INCOMING_MESSAGE -> (holder as IncomingMessageViewHolder).bind(chatItem)
} ChatItem.OUTGOING_FILE -> (holder as OutgoingFileViewHolder).bind(chatItem)
ChatItem.FILE -> { ChatItem.INCOMING_FILE -> (holder as IncomingFileViewHolder).bind(chatItem)
(holder as FileViewHolder).bind(chatItem, position, onSavingFile)
}
} }
} }

View File

@ -135,7 +135,7 @@ class AIRAService : Service() {
} }
} }
fun sendFilesFromUris(sessionId: Int, uris: List<Uri>): List<ByteArray>? { fun sendFilesFromUris(sessionId: Int, uris: List<Uri>) {
val files = mutableListOf<SendFile>() val files = mutableListOf<SendFile>()
var useLargeFileTransfer = false var useLargeFileTransfer = false
for (uri in uris) { for (uri in uris) {
@ -146,30 +146,22 @@ class AIRAService : Service() {
} }
} }
} }
return if (useLargeFileTransfer) { if (useLargeFileTransfer) {
sendLargeFilesTo(sessionId, files) sendLargeFilesTo(sessionId, files)
null
} else { } else {
val msgs = mutableListOf<ByteArray>()
for (file in files) { for (file in files) {
sendSmallFileTo(sessionId, file)?.let { msg -> sendSmallFileTo(sessionId, file)
msgs.add(msg)
}
} }
msgs
} }
} }
private fun sendSmallFileTo(sessionId: Int, sendFile: SendFile): ByteArray? { private fun sendSmallFileTo(sessionId: Int, sendFile: SendFile) {
val buffer = sendFile.inputStream.readBytes() val buffer = sendFile.inputStream.readBytes()
sendFile.inputStream.close() sendFile.inputStream.close()
sendTo(sessionId, Protocol.newFile(sendFile.fileName, buffer)) sendTo(sessionId, Protocol.newFile(sendFile.fileName, buffer))
AIRADatabase.storeFile(contacts[sessionId]?.uuid, buffer)?.let { rawFileUuid -> AIRADatabase.storeFile(contacts[sessionId]?.uuid, buffer)?.let { rawFileUuid ->
val msg = byteArrayOf(Protocol.FILE) + rawFileUuid + sendFile.fileName.toByteArray() saveMsg(sessionId, byteArrayOf(Protocol.FILE) + rawFileUuid + sendFile.fileName.toByteArray())
saveMsg(sessionId, msg)
return msg
} }
return null
} }
private fun sendLargeFilesTo(sessionId: Int, files: MutableList<SendFile>) { private fun sendLargeFilesTo(sessionId: Int, files: MutableList<SendFile>) {

View File

@ -11,10 +11,10 @@ jni = { version = "0.19", default-features = false }
crate-type = ["dylib"] crate-type = ["dylib"]
[dependencies] [dependencies]
rand-8 = {package = "rand", version = "0.8.3"} rand = "0.8.3"
rand-7 = {package = "rand", version = "0.7.3"} rand-7 = {package = "rand", version = "0.7.3"}
lazy_static = "1.4.0" lazy_static = "1.4.0"
rusqlite = {version = "0.25.1", features = ["bundled"]} rusqlite = { version = "0.25.1", features = ["bundled"] }
ed25519-dalek = "1" #for singing ed25519-dalek = "1" #for singing
x25519-dalek = "1.1" #for shared secret x25519-dalek = "1.1" #for shared secret
sha2 = "0.9.3" sha2 = "0.9.3"
@ -24,7 +24,7 @@ aes-gcm-siv = "0.10.0" #Database
hmac = "0.11.0" hmac = "0.11.0"
hex = "0.4.3" hex = "0.4.3"
strum_macros = "0.20.1" #display enums strum_macros = "0.20.1" #display enums
uuid = {version = "0.8", features = ["v4"]} uuid = { version = "0.8", features = ["v4"] }
scrypt = "0.7.0" scrypt = "0.7.0"
zeroize = "1.2.0" zeroize = "1.2.0"
log = "0.4.14" log = "0.4.14"

View File

@ -1,14 +1,12 @@
use std::convert::TryInto; use std::{convert::TryInto, fmt::Display};
use hkdf::Hkdf; use hkdf::Hkdf;
use sha2::Sha384; use sha2::Sha384;
use hmac::{Hmac, Mac, NewMac}; use hmac::{Hmac, NewMac, Mac};
use scrypt::{scrypt, Params}; use scrypt::{scrypt, Params};
use rand_8::{RngCore, rngs::OsRng}; use rand::{RngCore, rngs::OsRng};
use aes_gcm::{aead::Aead, NewAead, Nonce}; use aes_gcm::{aead::Aead, NewAead, Nonce};
use aes_gcm_siv::Aes256GcmSiv; use aes_gcm_siv::Aes256GcmSiv;
use zeroize::Zeroize; use zeroize::Zeroize;
use strum_macros::Display;
use crate::utils::*;
pub const HASH_OUTPUT_LEN: usize = 48; //SHA384 pub const HASH_OUTPUT_LEN: usize = 48; //SHA384
const KEY_LEN: usize = 16; const KEY_LEN: usize = 16;
@ -33,6 +31,26 @@ fn hkdf_expand_label(key: &[u8], label: &str, context: Option<&[u8]>, okm: &mut
}; };
} }
fn get_labels(handshake: bool, i_am_bob: bool) -> (String, String) {
let mut label = if handshake {
"handshake"
} else {
"application"
}.to_owned();
label += "_i_am_";
let local_label = label.clone() + if i_am_bob {
"bob"
} else {
"alice"
};
let peer_label = label + if i_am_bob {
"alice"
} else {
"bob"
};
(local_label, peer_label)
}
pub struct HandshakeKeys { pub struct HandshakeKeys {
pub local_key: [u8; KEY_LEN], pub local_key: [u8; KEY_LEN],
pub local_iv: [u8; IV_LEN], pub local_iv: [u8; IV_LEN],
@ -47,11 +65,11 @@ impl HandshakeKeys {
pub fn derive_keys(shared_secret: [u8; 32], handshake_hash: [u8; HASH_OUTPUT_LEN], i_am_bob: bool) -> HandshakeKeys { pub fn derive_keys(shared_secret: [u8; 32], handshake_hash: [u8; HASH_OUTPUT_LEN], i_am_bob: bool) -> HandshakeKeys {
let (handshake_secret, _) = Hkdf::<Sha384>::extract(None, &shared_secret); let (handshake_secret, _) = Hkdf::<Sha384>::extract(None, &shared_secret);
let local_label = "handshake".to_owned() + if i_am_bob {"i_am_bob"} else {"i_am_alice"}; let (local_label, peer_label) = get_labels(true, i_am_bob);
let mut local_handshake_traffic_secret = [0; HASH_OUTPUT_LEN]; let mut local_handshake_traffic_secret = [0; HASH_OUTPUT_LEN];
hkdf_expand_label(handshake_secret.as_slice(), &local_label, Some(&handshake_hash), &mut local_handshake_traffic_secret); hkdf_expand_label(handshake_secret.as_slice(), &local_label, Some(&handshake_hash), &mut local_handshake_traffic_secret);
let peer_label = "handshake".to_owned() + if i_am_bob {"i_am_alice"} else {"i_am_bob"};
let mut peer_handshake_traffic_secret = [0; HASH_OUTPUT_LEN]; let mut peer_handshake_traffic_secret = [0; HASH_OUTPUT_LEN];
hkdf_expand_label(handshake_secret.as_slice(), &peer_label, Some(&handshake_hash), &mut peer_handshake_traffic_secret); hkdf_expand_label(handshake_secret.as_slice(), &peer_label, Some(&handshake_hash), &mut peer_handshake_traffic_secret);
@ -72,7 +90,7 @@ impl HandshakeKeys {
peer_key: peer_handshake_key, peer_key: peer_handshake_key,
peer_iv: peer_handshake_iv, peer_iv: peer_handshake_iv,
peer_handshake_traffic_secret: peer_handshake_traffic_secret, peer_handshake_traffic_secret: peer_handshake_traffic_secret,
handshake_secret: to_array_48(handshake_secret.as_slice()) handshake_secret: handshake_secret.as_slice().try_into().unwrap(),
} }
} }
} }
@ -90,11 +108,11 @@ impl ApplicationKeys {
hkdf_expand_label(&handshake_secret, "derived", None, &mut derived_secret); hkdf_expand_label(&handshake_secret, "derived", None, &mut derived_secret);
let (master_secret, _) = Hkdf::<Sha384>::extract(Some(&derived_secret), b""); let (master_secret, _) = Hkdf::<Sha384>::extract(Some(&derived_secret), b"");
let local_label = "application".to_owned() + if i_am_bob {"i_am_bob"} else {"i_am_alice"}; let (local_label, peer_label) = get_labels(false, i_am_bob);
let mut local_application_traffic_secret = [0; HASH_OUTPUT_LEN]; let mut local_application_traffic_secret = [0; HASH_OUTPUT_LEN];
hkdf_expand_label(&master_secret, &local_label, Some(&handshake_hash), &mut local_application_traffic_secret); hkdf_expand_label(&master_secret, &local_label, Some(&handshake_hash), &mut local_application_traffic_secret);
let peer_label = "application".to_owned() + if i_am_bob {"i_am_alice"} else {"i_am_bob"};
let mut peer_application_traffic_secret = [0; HASH_OUTPUT_LEN]; let mut peer_application_traffic_secret = [0; HASH_OUTPUT_LEN];
hkdf_expand_label(&master_secret, &peer_label, Some(&handshake_hash), &mut peer_application_traffic_secret); hkdf_expand_label(&master_secret, &peer_label, Some(&handshake_hash), &mut peer_application_traffic_secret);
@ -161,12 +179,21 @@ pub fn encrypt_data(data: &[u8], master_key: &[u8]) -> Result<Vec<u8>, CryptoErr
Ok(cipher_text) Ok(cipher_text)
} }
#[derive(Display, Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum CryptoError { pub enum CryptoError {
DecryptionFailed, DecryptionFailed,
InvalidLength InvalidLength
} }
impl Display for CryptoError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
CryptoError::DecryptionFailed => "Decryption failed",
CryptoError::InvalidLength => "Invalid length",
})
}
}
pub fn decrypt_data(data: &[u8], master_key: &[u8]) -> Result<Vec<u8>, CryptoError> { pub fn decrypt_data(data: &[u8], master_key: &[u8]) -> Result<Vec<u8>, CryptoError> {
if data.len() <= IV_LEN || master_key.len() != MASTER_KEY_LEN { if data.len() <= IV_LEN || master_key.len() != MASTER_KEY_LEN {
return Err(CryptoError::InvalidLength); return Err(CryptoError::InvalidLength);

View File

@ -256,56 +256,57 @@ impl Identity {
match db.prepare(&format!("SELECT count(*) FROM \"{}\"", contact_uuid)) { match db.prepare(&format!("SELECT count(*) FROM \"{}\"", contact_uuid)) {
Ok(mut stmt) => { Ok(mut stmt) => {
let mut rows = stmt.query([]).unwrap(); let mut rows = stmt.query([]).unwrap();
let row = rows.next().unwrap(); match rows.next() {
if row.is_some() { Ok(row) => if row.is_some() {
let total: usize = row.unwrap().get(0).unwrap(); let total: usize = row.unwrap().get(0).unwrap();
if offset >= total { if offset >= total {
print_error!("Offset larger than total numbers of rows"); None
None } else {
} else { if offset+count >= total {
if offset+count >= total { count = total-offset;
count = total-offset; }
} match db.prepare(&format!("SELECT outgoing, data FROM \"{}\" LIMIT {} OFFSET {}", contact_uuid, count, total-offset-count)) {
match db.prepare(&format!("SELECT outgoing, data FROM \"{}\" LIMIT {} OFFSET {}", contact_uuid, count, total-offset-count)) { Ok(mut stmt) => {
Ok(mut stmt) => { let mut rows = stmt.query([]).unwrap();
let mut rows = stmt.query([]).unwrap(); let mut msgs = Vec::new();
let mut msgs = Vec::new(); while let Some(row) = rows.next().unwrap() {
while let Some(row) = rows.next().unwrap() { let encrypted_outgoing: Vec<u8> = row.get(0).unwrap();
let encrypted_outgoing: Vec<u8> = row.get(0).unwrap(); match crypto::decrypt_data(encrypted_outgoing.as_slice(), &self.master_key){
match crypto::decrypt_data(encrypted_outgoing.as_slice(), &self.master_key){ Ok(outgoing) => {
Ok(outgoing) => { match byte_to_bool(outgoing[0]) {
match byte_to_bool(outgoing[0]) { Ok(outgoing) => {
Ok(outgoing) => { let encrypted_data: Vec<u8> = row.get(1).unwrap();
let encrypted_data: Vec<u8> = row.get(1).unwrap(); match crypto::decrypt_data(encrypted_data.as_slice(), &self.master_key) {
match crypto::decrypt_data(encrypted_data.as_slice(), &self.master_key) { Ok(data) => {
Ok(data) => { msgs.push(
msgs.push( (
( outgoing,
outgoing, data
data )
) )
) },
}, Err(e) => print_error!(e)
Err(e) => print_error!(e) }
} }
Err(_) => {}
} }
Err(_) => {}
} }
Err(e) => print_error!(e)
} }
Err(e) => print_error!(e)
} }
Some(msgs)
}
Err(e) => {
print_error!(e);
None
} }
Some(msgs)
}
Err(e) => {
print_error!(e);
None
} }
} }
} else {
None
} }
} else { Err(_) => None
None
} }
} }
Err(e) => { Err(e) => {

View File

@ -2,10 +2,6 @@ use std::convert::TryInto;
use uuid::Bytes; use uuid::Bytes;
use crate::print_error; use crate::print_error;
pub fn to_array_48(s: &[u8]) -> [u8; 48] {
s.try_into().unwrap()
}
pub fn to_uuid_bytes(bytes: &[u8]) -> Option<Bytes> { pub fn to_uuid_bytes(bytes: &[u8]) -> Option<Bytes> {
match bytes.try_into() { match bytes.try_into() {
Ok(uuid) => Some(uuid), Ok(uuid) => Some(uuid),

View File

@ -6,13 +6,13 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".ChatActivity"> tools:context=".ChatActivity">
<include layout="@layout/profile_toolbar" android:id="@+id/toolbar"/>
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_chat" android:id="@+id/recycler_chat"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:paddingHorizontal="20dp" app:layout_constraintTop_toBottomOf="@id/toolbar"
android:paddingBottom="20dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/bottom_panel"/> app:layout_constraintBottom_toTopOf="@id/bottom_panel"/>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
@ -20,7 +20,9 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
android:visibility="gone"> android:visibility="gone"
android:paddingVertical="5dp"
android:background="@color/primary">
<ImageButton <ImageButton
android:id="@+id/button_attach" android:id="@+id/button_attach"

View File

@ -1,79 +1,93 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/refresher"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".MainActivity"> tools:context=".MainActivity">
<androidx.constraintlayout.widget.ConstraintLayout <include layout="@layout/profile_toolbar" android:id="@+id/toolbar"/>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/refresher"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/toolbar"
<TextView app:layout_constraintBottom_toBottomOf="parent">
android:id="@+id/text_online_sessions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/online_peers"
style="@style/Label"
app:layout_constraintTop_toTopOf="parent"/>
<ListView
android:id="@+id/online_sessions"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/text_online_sessions"/>
<TextView
android:id="@+id/text_offline_sessions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/offline_contacts"
style="@style/Label"
app:layout_constraintTop_toBottomOf="@id/online_sessions"/>
<ListView
android:id="@+id/offline_sessions"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/text_offline_sessions"
app:layout_constraintBottom_toTopOf="@+id/bottom_panel"/>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/bottom_panel"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent">
android:orientation="horizontal"
android:layout_marginHorizontal="10dp"
app:layout_constraintBottom_toBottomOf="parent">
<EditText <TextView
android:id="@+id/edit_peer_ip" android:id="@+id/text_online_sessions"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:inputType="text" android:text="@string/online_peers"
android:maxLines="1" style="@style/Label"
android:imeOptions="actionGo" app:layout_constraintTop_toTopOf="parent"/>
android:hint="@string/add_peer_ip"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/button_show_ip"/>
<ImageButton <ListView
android:id="@+id/button_show_ip" android:id="@+id/online_sessions"
android:layout_width="@dimen/image_button_size" android:layout_width="match_parent"
android:layout_height="@dimen/image_button_size" android:layout_height="0dp"
android:src="@drawable/ic_info" android:divider="@color/transparent"
style="@style/ImageButton" app:layout_constraintTop_toBottomOf="@id/text_online_sessions"/>
app:layout_constraintTop_toTopOf="parent"
<TextView
android:id="@+id/text_offline_sessions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/offline_contacts"
style="@style/Label"
app:layout_constraintTop_toBottomOf="@id/online_sessions"/>
<ListView
android:id="@+id/offline_sessions"
android:layout_width="match_parent"
android:layout_height="0dp"
android:divider="@color/transparent"
app:layout_constraintTop_toBottomOf="@id/text_offline_sessions"
app:layout_constraintBottom_toTopOf="@+id/bottom_panel"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/bottom_panel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/edit_peer_ip" android:paddingHorizontal="10dp"
app:layout_constraintEnd_toEndOf="parent"/> android:paddingVertical="5dp"
android:background="@color/primary">
<EditText
android:id="@+id/edit_peer_ip"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:inputType="text"
android:maxLines="1"
android:imeOptions="actionGo"
android:hint="@string/add_peer_ip"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/button_show_ip"/>
<ImageButton
android:id="@+id/button_show_ip"
android:layout_width="@dimen/image_button_size"
android:layout_height="@dimen/image_button_size"
android:src="@drawable/ic_info"
style="@style/ImageButton"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/edit_peer_ip"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -4,7 +4,7 @@
android:orientation="horizontal" android:orientation="horizontal"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:padding="10dp"> android:padding="15dp">
<sushi.hardcore.aira.widgets.TextAvatar <sushi.hardcore.aira.widgets.TextAvatar
android:id="@+id/text_avatar" android:id="@+id/text_avatar"

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="@color/primary"
app:contentInsetStartWithNavigation="0dp"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="match_parent">
<sushi.hardcore.aira.widgets.TextAvatar
android:id="@+id/text_avatar"
android:layout_width="40dp"
android:layout_height="40dp"
app:textSize="8sp"
android:layout_gravity="center_vertical"/>
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Widget.ActionBar.Title"
android:layout_marginStart="10dp"
android:layout_gravity="center_vertical"/>
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.Toolbar>

View File

@ -4,7 +4,7 @@
<color name="secondary">#19a52c</color> <color name="secondary">#19a52c</color>
<color name="white">#FFFFFF</color> <color name="white">#FFFFFF</color>
<color name="backgroundColor">#111111</color> <color name="backgroundColor">#111111</color>
<color name="sessionBackground">#1F1F1F</color> <color name="sessionBackground">#1A1A1A</color>
<color name="bubbleBackground">@color/sessionBackground</color> <color name="bubbleBackground">@color/sessionBackground</color>
<color name="incomingBubbleBackground">@color/secondary</color> <color name="incomingBubbleBackground">@color/secondary</color>
<color name="textLink">#3845A3</color> <color name="textLink">#3845A3</color>

View File

@ -1,18 +1,11 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.AIRA" parent="Theme.AppCompat"> <style name="Theme.AIRA" parent="Theme.AppCompat">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/primary</item> <item name="colorPrimary">@color/primary</item>
<item name="colorPrimaryVariant">@color/primary</item> <item name="colorPrimaryVariant">@color/primary</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/secondary</item> <item name="colorSecondary">@color/secondary</item>
<item name="colorSecondaryVariant">@color/secondary</item> <item name="colorSecondaryVariant">@color/secondary</item>
<item name="colorAccent">@color/secondary</item> <item name="colorAccent">@color/secondary</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item> <item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<item name="android:windowBackground">@color/backgroundColor</item> <item name="android:windowBackground">@color/backgroundColor</item>
</style> </style>
</resources> </resources>

View File

@ -1,12 +1,12 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext.kotlin_version = "1.4.32" ext.kotlin_version = "1.5.0"
repositories { repositories {
google() google()
jcenter() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:4.2.0' classpath 'com.android.tools.build:gradle:4.2.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
@ -17,7 +17,7 @@ buildscript {
allprojects { allprojects {
repositories { repositories {
google() google()
jcenter() mavenCentral()
} }
} }