mirror of
https://github.com/chatmail/core.git
synced 2026-04-05 23:22:11 +03:00
Compare commits
12 Commits
1.0.0-beta
...
config_ung
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00bb1b25a3 | ||
|
|
35fa0a7271 | ||
|
|
1fae0bddc7 | ||
|
|
236ecd6d67 | ||
|
|
135048d63b | ||
|
|
d2cb1658e3 | ||
|
|
2e3269dedf | ||
|
|
bfc67c705a | ||
|
|
8cd9a5dcb3 | ||
|
|
d0270108e8 | ||
|
|
ae612e07dc | ||
|
|
ff6a4d18cf |
@@ -1320,12 +1320,13 @@ pub unsafe extern "C" fn dc_get_securejoin_qr(
|
||||
) -> *mut libc::c_char {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_get_securejoin_qr()");
|
||||
return dc_strdup(ptr::null());
|
||||
return "".strdup();
|
||||
}
|
||||
|
||||
let context = &*context;
|
||||
|
||||
dc_securejoin::dc_get_securejoin_qr(context, chat_id)
|
||||
securejoin::dc_get_securejoin_qr(context, chat_id)
|
||||
.unwrap_or("".to_string())
|
||||
.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -1340,7 +1341,7 @@ pub unsafe extern "C" fn dc_join_securejoin(
|
||||
|
||||
let context = &*context;
|
||||
|
||||
dc_securejoin::dc_join_securejoin(context, qr)
|
||||
securejoin::dc_join_securejoin(context, as_str(qr))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
|
||||
@@ -14,6 +14,8 @@ extern crate lazy_static;
|
||||
extern crate rusqlite;
|
||||
|
||||
use std::borrow::Cow::{self, Borrowed, Owned};
|
||||
use std::io::{self, Write};
|
||||
use std::process::Command;
|
||||
use std::ptr;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
@@ -22,10 +24,10 @@ use deltachat::config;
|
||||
use deltachat::configure::*;
|
||||
use deltachat::constants::*;
|
||||
use deltachat::context::*;
|
||||
use deltachat::dc_securejoin::*;
|
||||
use deltachat::dc_tools::*;
|
||||
use deltachat::job::*;
|
||||
use deltachat::oauth2::*;
|
||||
use deltachat::securejoin::*;
|
||||
use deltachat::types::*;
|
||||
use deltachat::x::*;
|
||||
use rustyline::completion::{Completer, FilenameCompleter, Pair};
|
||||
@@ -516,30 +518,27 @@ unsafe fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult
|
||||
}
|
||||
"getqr" | "getbadqr" => {
|
||||
start_threads(ctx.clone());
|
||||
let qrstr =
|
||||
dc_get_securejoin_qr(&ctx.read().unwrap(), arg1.parse().unwrap_or_default());
|
||||
if !qrstr.is_null() && 0 != *qrstr.offset(0isize) as libc::c_int {
|
||||
if arg0 == "getbadqr" && strlen(qrstr) > 40 {
|
||||
let mut i: libc::c_int = 12i32;
|
||||
while i < 22i32 {
|
||||
*qrstr.offset(i as isize) = '0' as i32 as libc::c_char;
|
||||
i += 1
|
||||
if let Some(mut qr) =
|
||||
dc_get_securejoin_qr(&ctx.read().unwrap(), arg1.parse().unwrap_or_default())
|
||||
{
|
||||
if !qr.is_empty() {
|
||||
if arg0 == "getbadqr" && qr.len() > 40 {
|
||||
qr.replace_range(12..22, "0000000000")
|
||||
}
|
||||
println!("{}", qr);
|
||||
let output = Command::new("qrencode")
|
||||
.args(&["-t", "ansiutf8", qr.as_str(), "-o", "-"])
|
||||
.output()
|
||||
.expect("failed to execute process");
|
||||
io::stdout().write_all(&output.stdout).unwrap();
|
||||
io::stderr().write_all(&output.stderr).unwrap();
|
||||
}
|
||||
println!("{}", to_string(qrstr as *const _));
|
||||
let syscmd = dc_mprintf(
|
||||
b"qrencode -t ansiutf8 \"%s\" -o -\x00" as *const u8 as *const libc::c_char,
|
||||
qrstr,
|
||||
);
|
||||
system(syscmd);
|
||||
free(syscmd as *mut libc::c_void);
|
||||
}
|
||||
free(qrstr as *mut libc::c_void);
|
||||
}
|
||||
"joinqr" => {
|
||||
start_threads(ctx.clone());
|
||||
if !arg0.is_empty() {
|
||||
dc_join_securejoin(&ctx.read().unwrap(), arg1_c);
|
||||
dc_join_securejoin(&ctx.read().unwrap(), arg1);
|
||||
}
|
||||
}
|
||||
"exit" | "quit" => return Ok(ExitResult::Exit),
|
||||
|
||||
@@ -14,7 +14,7 @@ except ImportError:
|
||||
import deltachat
|
||||
from . import const
|
||||
from .capi import ffi, lib
|
||||
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array
|
||||
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array, DCLot
|
||||
from .chatting import Contact, Chat, Message
|
||||
|
||||
|
||||
@@ -329,6 +329,56 @@ class Account(object):
|
||||
raise RuntimeError("could not send out autocrypt setup message")
|
||||
return from_dc_charpointer(res)
|
||||
|
||||
def get_setup_contact_qr(self):
|
||||
""" get/create Setup-Contact QR Code as ascii-string.
|
||||
|
||||
this string needs to be transferred to another DC account
|
||||
in a second channel (typically used by mobiles with QRcode-show + scan UX)
|
||||
where qr_setup_contact(qr) is called.
|
||||
"""
|
||||
res = lib.dc_get_securejoin_qr(self._dc_context, 0)
|
||||
return from_dc_charpointer(res)
|
||||
|
||||
def check_qr(self, qr):
|
||||
""" check qr code and return :class:`ScannedQRCode` instance representing the result"""
|
||||
res = ffi.gc(
|
||||
lib.dc_check_qr(self._dc_context, as_dc_charpointer(qr)),
|
||||
lib.dc_lot_unref
|
||||
)
|
||||
lot = DCLot(res)
|
||||
if lot.state() == const.DC_QR_ERROR:
|
||||
raise ValueError("invalid or unknown QR code: {}".format(lot.text1()))
|
||||
return ScannedQRCode(lot)
|
||||
|
||||
def qr_setup_contact(self, qr):
|
||||
""" setup contact and return a Chat after contact is established.
|
||||
|
||||
Note that this function may block for a long time as messages are exchanged
|
||||
with the emitter of the QR code. On success a :class:`deltachat.chatting.Chat` instance
|
||||
is returned.
|
||||
:param qr: valid "setup contact" QR code (all other QR codes will result in an exception)
|
||||
"""
|
||||
assert self.check_qr(qr).is_ask_verifycontact()
|
||||
chat_id = lib.dc_join_securejoin(self._dc_context, as_dc_charpointer(qr))
|
||||
if chat_id == 0:
|
||||
raise ValueError("could not setup secure contact")
|
||||
return Chat(self, chat_id)
|
||||
|
||||
def qr_join_chat(self, qr):
|
||||
""" join a chat group through a QR code.
|
||||
|
||||
Note that this function may block for a long time as messages are exchanged
|
||||
with the emitter of the QR code. On success a :class:`deltachat.chatting.Chat` instance
|
||||
is returned which is the chat that we just joined.
|
||||
|
||||
:param qr: valid "join-group" QR code (all other QR codes will result in an exception)
|
||||
"""
|
||||
assert self.check_qr(qr).is_ask_verifygroup()
|
||||
chat_id = lib.dc_join_securejoin(self._dc_context, as_dc_charpointer(qr))
|
||||
if chat_id == 0:
|
||||
raise ValueError("could not join group")
|
||||
return Chat(self, chat_id)
|
||||
|
||||
def start_threads(self):
|
||||
""" start IMAP/SMTP threads (and configure account if it hasn't happened).
|
||||
|
||||
@@ -492,3 +542,18 @@ def _destroy_dc_context(dc_context, dc_context_unref=lib.dc_context_unref):
|
||||
# we are deep into Python Interpreter shutdown,
|
||||
# so no need to clear the callback context mapping.
|
||||
pass
|
||||
|
||||
|
||||
class ScannedQRCode:
|
||||
def __init__(self, dc_lot):
|
||||
self._dc_lot = dc_lot
|
||||
|
||||
def is_ask_verifycontact(self):
|
||||
return self._dc_lot.state() == const.DC_QR_ASK_VERIFYCONTACT
|
||||
|
||||
def is_ask_verifygroup(self):
|
||||
return self._dc_lot.state() == const.DC_QR_ASK_VERIFYGROUP
|
||||
|
||||
@property
|
||||
def contact_id(self):
|
||||
return self._dc_lot.id()
|
||||
|
||||
@@ -131,6 +131,16 @@ class Chat(object):
|
||||
"""
|
||||
return lib.dc_chat_get_type(self._dc_chat)
|
||||
|
||||
def get_join_qr(self):
|
||||
""" get/create Join-Group QR Code as ascii-string.
|
||||
|
||||
this string needs to be transferred to another DC account
|
||||
in a second channel (typically used by mobiles with QRcode-show + scan UX)
|
||||
where account.join_with_qrcode(qr) needs to be called.
|
||||
"""
|
||||
res = lib.dc_get_securejoin_qr(self._dc_context, self.id)
|
||||
return from_dc_charpointer(res)
|
||||
|
||||
# ------ chat messaging API ------------------------------
|
||||
|
||||
def send_text(self, text):
|
||||
|
||||
@@ -13,6 +13,15 @@ DC_GCL_NO_SPECIALS = 0x02
|
||||
DC_GCL_ADD_ALLDONE_HINT = 0x04
|
||||
DC_GCL_VERIFIED_ONLY = 0x01
|
||||
DC_GCL_ADD_SELF = 0x02
|
||||
DC_QR_ASK_VERIFYCONTACT = 200
|
||||
DC_QR_ASK_VERIFYGROUP = 202
|
||||
DC_QR_FPR_OK = 210
|
||||
DC_QR_FPR_MISMATCH = 220
|
||||
DC_QR_FPR_WITHOUT_ADDR = 230
|
||||
DC_QR_ADDR = 320
|
||||
DC_QR_TEXT = 330
|
||||
DC_QR_URL = 332
|
||||
DC_QR_ERROR = 400
|
||||
DC_CHAT_ID_DEADDROP = 1
|
||||
DC_CHAT_ID_TRASH = 3
|
||||
DC_CHAT_ID_MSGS_IN_CREATION = 4
|
||||
@@ -69,15 +78,13 @@ DC_EVENT_IMEX_FILE_WRITTEN = 2052
|
||||
DC_EVENT_SECUREJOIN_INVITER_PROGRESS = 2060
|
||||
DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061
|
||||
DC_EVENT_GET_STRING = 2091
|
||||
DC_EVENT_HTTP_GET = 2100
|
||||
DC_EVENT_HTTP_POST = 2110
|
||||
DC_EVENT_FILE_COPIED = 2055
|
||||
DC_EVENT_IS_OFFLINE = 2081
|
||||
# end const generated
|
||||
|
||||
|
||||
def read_event_defines(f):
|
||||
rex = re.compile(r'#define\s+((?:DC_EVENT_|DC_MSG|DC_STATE_|DC_CONTACT_ID_|DC_GCL|DC_CHAT)\S+)\s+([x\d]+).*')
|
||||
rex = re.compile(r'#define\s+((?:DC_EVENT_|DC_QR|DC_MSG|DC_STATE_|DC_CONTACT_ID_|DC_GCL|DC_CHAT)\S+)\s+([x\d]+).*')
|
||||
for line in f:
|
||||
m = rex.match(line)
|
||||
if m:
|
||||
@@ -90,7 +97,7 @@ if __name__ == "__main__":
|
||||
if len(sys.argv) >= 2:
|
||||
deltah = sys.argv[1]
|
||||
else:
|
||||
deltah = joinpath(dirname(dirname(dirname(here_dir))), "src", "deltachat.h")
|
||||
deltah = joinpath(dirname(dirname(dirname(here_dir))), "deltachat-ffi", "deltachat.h")
|
||||
assert os.path.exists(deltah)
|
||||
|
||||
lines = []
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .capi import lib
|
||||
from .capi import ffi
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def as_dc_charpointer(obj):
|
||||
@@ -17,3 +18,29 @@ def iter_array(dc_array_t, constructor):
|
||||
|
||||
def from_dc_charpointer(obj):
|
||||
return ffi.string(ffi.gc(obj, lib.dc_str_unref)).decode("utf8")
|
||||
|
||||
|
||||
class DCLot:
|
||||
def __init__(self, dc_lot):
|
||||
self._dc_lot = dc_lot
|
||||
|
||||
def id(self):
|
||||
return lib.dc_lot_get_id(self._dc_lot)
|
||||
|
||||
def state(self):
|
||||
return lib.dc_lot_get_state(self._dc_lot)
|
||||
|
||||
def text1(self):
|
||||
return from_dc_charpointer(lib.dc_lot_get_text1(self._dc_lot))
|
||||
|
||||
def text1_meaning(self):
|
||||
return lib.dc_lot_get_text1_meaning(self._dc_lot)
|
||||
|
||||
def text2(self):
|
||||
return from_dc_charpointer(lib.dc_lot_get_text2(self._dc_lot))
|
||||
|
||||
def timestamp(self):
|
||||
ts = lib.dc_lot_get_timestamp(self._dc_lot)
|
||||
if ts == 0:
|
||||
return None
|
||||
return datetime.utcfromtimestamp(ts)
|
||||
|
||||
@@ -213,6 +213,15 @@ def wait_configuration_progress(account, target):
|
||||
break
|
||||
|
||||
|
||||
def wait_securejoin_inviter_progress(account, target):
|
||||
while 1:
|
||||
evt_name, data1, data2 = \
|
||||
account._evlogger.get_matching("DC_EVENT_SECUREJOIN_INVITER_PROGRESS")
|
||||
if data2 >= target:
|
||||
print("** SECUREJOINT-INVITER PROGRESS {}".format(target), account)
|
||||
break
|
||||
|
||||
|
||||
def wait_successful_IMAP_SMTP_connection(account):
|
||||
imap_ok = smtp_ok = False
|
||||
while not imap_ok or not smtp_ok:
|
||||
|
||||
@@ -4,7 +4,7 @@ import os
|
||||
from deltachat import const, Account
|
||||
from deltachat.message import Message
|
||||
from datetime import datetime, timedelta
|
||||
from conftest import wait_configuration_progress, wait_successful_IMAP_SMTP_connection
|
||||
from conftest import wait_configuration_progress, wait_successful_IMAP_SMTP_connection, wait_securejoin_inviter_progress
|
||||
|
||||
|
||||
class TestOfflineAccountBasic:
|
||||
@@ -140,6 +140,13 @@ class TestOfflineChat:
|
||||
chat.set_name("title2")
|
||||
assert chat.get_name() == "title2"
|
||||
|
||||
@pytest.mark.parametrize("verified", [True, False])
|
||||
def test_group_chat_qr(self, acfactory, ac1, verified):
|
||||
ac2 = acfactory.get_configured_offline_account()
|
||||
chat = ac1.create_group_chat(name="title1", verified=verified)
|
||||
qr = chat.get_join_qr()
|
||||
assert ac2.check_qr(qr).is_ask_verifygroup
|
||||
|
||||
def test_delete_and_send_fails(self, ac1, chat1):
|
||||
chat1.delete()
|
||||
ac1._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
@@ -306,6 +313,16 @@ class TestOfflineChat:
|
||||
chat1.set_draft(None)
|
||||
assert chat1.get_draft() is None
|
||||
|
||||
def test_qr_setup_contact(self, acfactory, lp):
|
||||
ac1 = acfactory.get_configured_offline_account()
|
||||
ac2 = acfactory.get_configured_offline_account()
|
||||
qr = ac1.get_setup_contact_qr()
|
||||
assert qr.startswith("OPENPGP4FPR:")
|
||||
res = ac2.check_qr(qr)
|
||||
assert res.is_ask_verifycontact()
|
||||
assert not res.is_ask_verifygroup()
|
||||
assert res.contact_id == 10
|
||||
|
||||
|
||||
class TestOnlineAccount:
|
||||
def test_one_account_init(self, acfactory):
|
||||
@@ -546,3 +563,29 @@ class TestOnlineAccount:
|
||||
print("*************** Setup Code: ", setup_code)
|
||||
msg.continue_key_transfer(setup_code)
|
||||
assert ac1.get_info()["fingerprint"] == ac2.get_info()["fingerprint"]
|
||||
|
||||
def test_qr_setup_contact(self, acfactory, lp):
|
||||
ac1 = acfactory.get_online_configuring_account()
|
||||
ac2 = acfactory.get_online_configuring_account()
|
||||
wait_configuration_progress(ac2, 1000)
|
||||
wait_configuration_progress(ac1, 1000)
|
||||
lp.sec("ac1: create QR code and let ac2 scan it, starting the securejoin")
|
||||
qr = ac1.get_setup_contact_qr()
|
||||
lp.sec("ac2: start QR-code based setup contact protocol")
|
||||
ch = ac2.qr_setup_contact(qr)
|
||||
assert ch.id >= 10
|
||||
wait_securejoin_inviter_progress(ac1, 1000)
|
||||
|
||||
def test_qr_join_chat(self, acfactory, lp):
|
||||
ac1 = acfactory.get_online_configuring_account()
|
||||
ac2 = acfactory.get_online_configuring_account()
|
||||
wait_configuration_progress(ac2, 1000)
|
||||
wait_configuration_progress(ac1, 1000)
|
||||
lp.sec("ac1: create QR code and let ac2 scan it, starting the securejoin")
|
||||
|
||||
chat = ac1.create_group_chat("hello")
|
||||
qr = chat.get_join_qr()
|
||||
lp.sec("ac2: start QR-code based join-group protocol")
|
||||
ch = ac2.qr_join_chat(qr)
|
||||
assert ch.id >= 10
|
||||
wait_securejoin_inviter_progress(ac1, 1000)
|
||||
|
||||
30
src/chat.rs
30
src/chat.rs
@@ -854,7 +854,7 @@ pub fn unarchive(context: &Context, chat_id: u32) -> Result<(), Error> {
|
||||
/// However, this does not imply, the message really reached the recipient -
|
||||
/// sending may be delayed eg. due to network problems. However, from your
|
||||
/// view, you're done with the message. Sooner or later it will find its way.
|
||||
pub unsafe fn send_msg<'a>(
|
||||
pub fn send_msg<'a>(
|
||||
context: &'a Context,
|
||||
chat_id: u32,
|
||||
msg: &mut Message<'a>,
|
||||
@@ -872,7 +872,7 @@ pub unsafe fn send_msg<'a>(
|
||||
}
|
||||
|
||||
ensure!(
|
||||
job_send_msg(context, msg.id) != 0,
|
||||
unsafe { job_send_msg(context, msg.id) } != 0,
|
||||
"Failed to initiate send job"
|
||||
);
|
||||
|
||||
@@ -1394,7 +1394,7 @@ pub unsafe fn add_contact_to_chat(context: &Context, chat_id: u32, contact_id: u
|
||||
|
||||
// TODO should return bool /rtn
|
||||
#[allow(non_snake_case)]
|
||||
pub unsafe fn add_contact_to_chat_ex(
|
||||
pub fn add_contact_to_chat_ex(
|
||||
context: &Context,
|
||||
chat_id: u32,
|
||||
contact_id: u32,
|
||||
@@ -1407,7 +1407,7 @@ pub unsafe fn add_contact_to_chat_ex(
|
||||
if contact.is_err() || chat_id <= DC_CHAT_ID_LAST_SPECIAL {
|
||||
return 0;
|
||||
}
|
||||
let mut msg = dc_msg_new_untyped(context);
|
||||
let mut msg = unsafe { dc_msg_new_untyped(context) };
|
||||
|
||||
reset_gossiped_timestamp(context, chat_id);
|
||||
let contact = contact.unwrap();
|
||||
@@ -1918,16 +1918,7 @@ pub fn get_chat_cnt(context: &Context) -> usize {
|
||||
}
|
||||
}
|
||||
|
||||
pub unsafe fn get_chat_id_by_grpid(
|
||||
context: &Context,
|
||||
grpid: impl AsRef<str>,
|
||||
ret_blocked: Option<&mut Blocked>,
|
||||
ret_verified: *mut libc::c_int,
|
||||
) -> u32 {
|
||||
if !ret_verified.is_null() {
|
||||
*ret_verified = 0;
|
||||
}
|
||||
|
||||
pub fn get_chat_id_by_grpid(context: &Context, grpid: impl AsRef<str>) -> (u32, bool, Blocked) {
|
||||
context
|
||||
.sql
|
||||
.query_row(
|
||||
@@ -1936,17 +1927,12 @@ pub unsafe fn get_chat_id_by_grpid(
|
||||
|row| {
|
||||
let chat_id = row.get(0)?;
|
||||
|
||||
if let Some(b) = ret_blocked {
|
||||
*b = row.get::<_, Option<Blocked>>(1)?.unwrap_or_default();
|
||||
}
|
||||
|
||||
let b = row.get::<_, Option<Blocked>>(1)?.unwrap_or_default();
|
||||
let v = row.get::<_, Option<Chattype>>(2)?.unwrap_or_default();
|
||||
*ret_verified = (v == Chattype::VerifiedGroup) as libc::c_int;
|
||||
|
||||
Ok(chat_id)
|
||||
Ok((chat_id, v == Chattype::VerifiedGroup, b))
|
||||
},
|
||||
)
|
||||
.unwrap_or_default()
|
||||
.unwrap_or((0, false, Blocked::Not))
|
||||
}
|
||||
|
||||
pub fn add_device_msg(context: &Context, chat_id: u32, text: impl AsRef<str>) {
|
||||
|
||||
@@ -33,7 +33,7 @@ macro_rules! progress {
|
||||
|
||||
// connect
|
||||
pub unsafe fn configure(context: &Context) {
|
||||
if 0 != dc_has_ongoing(context) {
|
||||
if dc_has_ongoing(context) {
|
||||
warn!(
|
||||
context,
|
||||
0, "There is already another ongoing process running.",
|
||||
@@ -43,6 +43,7 @@ pub unsafe fn configure(context: &Context) {
|
||||
job_kill_action(context, Action::ConfigureImap);
|
||||
job_add(context, Action::ConfigureImap, 0, Params::new(), 0);
|
||||
}
|
||||
|
||||
/// Check if the context is already configured.
|
||||
pub fn dc_is_configured(context: &Context) -> libc::c_int {
|
||||
if context
|
||||
@@ -56,17 +57,6 @@ pub fn dc_is_configured(context: &Context) -> libc::c_int {
|
||||
0
|
||||
}
|
||||
}
|
||||
/// Check if there is an ongoing process.
|
||||
unsafe fn dc_has_ongoing(context: &Context) -> libc::c_int {
|
||||
let s_a = context.running_state.clone();
|
||||
let s = s_a.read().unwrap();
|
||||
|
||||
if s.ongoing_running || !s.shall_stop_ongoing {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* Configure JOB
|
||||
@@ -80,7 +70,7 @@ pub unsafe fn dc_job_do_DC_JOB_CONFIGURE_IMAP(context: &Context, _job: &Job) {
|
||||
let mut ongoing_allocated_here = false;
|
||||
|
||||
let mut param_autoconfig: Option<dc_loginparam_t> = None;
|
||||
if !(0 == dc_alloc_ongoing(context)) {
|
||||
if dc_alloc_ongoing(context) {
|
||||
ongoing_allocated_here = true;
|
||||
if !context.sql.is_open() {
|
||||
error!(context, 0, "Cannot configure, database not opened.",);
|
||||
@@ -591,7 +581,43 @@ pub unsafe fn dc_job_do_DC_JOB_CONFIGURE_IMAP(context: &Context, _job: &Job) {
|
||||
progress!(context, (if success { 1000 } else { 0 }));
|
||||
}
|
||||
|
||||
/* File Structure like in C: */
|
||||
/*******************************************************************************
|
||||
* Ongoing process allocation/free/check
|
||||
******************************************************************************/
|
||||
|
||||
pub fn dc_alloc_ongoing(context: &Context) -> bool {
|
||||
if dc_has_ongoing(context) {
|
||||
warn!(
|
||||
context,
|
||||
0, "There is already another ongoing process running.",
|
||||
);
|
||||
|
||||
false
|
||||
} else {
|
||||
let s_a = context.running_state.clone();
|
||||
let mut s = s_a.write().unwrap();
|
||||
|
||||
s.ongoing_running = true;
|
||||
s.shall_stop_ongoing = false;
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dc_free_ongoing(context: &Context) {
|
||||
let s_a = context.running_state.clone();
|
||||
let mut s = s_a.write().unwrap();
|
||||
|
||||
s.ongoing_running = false;
|
||||
s.shall_stop_ongoing = true;
|
||||
}
|
||||
|
||||
fn dc_has_ongoing(context: &Context) -> bool {
|
||||
let s_a = context.running_state.clone();
|
||||
let s = s_a.read().unwrap();
|
||||
|
||||
s.ongoing_running || !s.shall_stop_ongoing
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* Connect to configured account
|
||||
@@ -624,35 +650,6 @@ pub fn dc_connect_to_configured_imap(context: &Context, imap: &Imap) -> libc::c_
|
||||
* Configure a Context
|
||||
******************************************************************************/
|
||||
|
||||
/// Request an ongoing process to start.
|
||||
/// Returns 0=process started, 1=not started, there is running another process
|
||||
pub unsafe fn dc_alloc_ongoing(context: &Context) -> libc::c_int {
|
||||
if 0 != dc_has_ongoing(context) {
|
||||
warn!(
|
||||
context,
|
||||
0, "There is already another ongoing process running.",
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
let s_a = context.running_state.clone();
|
||||
let mut s = s_a.write().unwrap();
|
||||
|
||||
s.ongoing_running = true;
|
||||
s.shall_stop_ongoing = false;
|
||||
|
||||
1
|
||||
}
|
||||
|
||||
/// Frees the process allocated with dc_alloc_ongoing() - independently of dc_shall_stop_ongoing.
|
||||
/// If dc_alloc_ongoing() fails, this function MUST NOT be called.
|
||||
pub unsafe fn dc_free_ongoing(context: &Context) {
|
||||
let s_a = context.running_state.clone();
|
||||
let mut s = s_a.write().unwrap();
|
||||
|
||||
s.ongoing_running = false;
|
||||
s.shall_stop_ongoing = true;
|
||||
}
|
||||
|
||||
/// Signal an ongoing process to stop.
|
||||
pub fn dc_stop_ongoing_process(context: &Context) {
|
||||
let s_a = context.running_state.clone();
|
||||
|
||||
@@ -42,7 +42,7 @@ impl Default for Blocked {
|
||||
|
||||
pub const DC_IMAP_SEEN: u32 = 0x1;
|
||||
|
||||
const DC_HANDSHAKE_CONTINUE_NORMAL_PROCESSING: i32 = 0x01;
|
||||
pub const DC_HANDSHAKE_CONTINUE_NORMAL_PROCESSING: i32 = 0x01;
|
||||
pub const DC_HANDSHAKE_STOP_NORMAL_PROCESSING: i32 = 0x02;
|
||||
pub const DC_HANDSHAKE_ADD_DELETE_JOB: i32 = 0x04;
|
||||
|
||||
@@ -169,6 +169,12 @@ const DC_LP_IMAP_SOCKET_FLAGS: usize =
|
||||
const DC_LP_SMTP_SOCKET_FLAGS: usize =
|
||||
(DC_LP_SMTP_SOCKET_STARTTLS | DC_LP_SMTP_SOCKET_SSL | DC_LP_SMTP_SOCKET_PLAIN);
|
||||
|
||||
// QR code scanning (view from Bob, the joiner)
|
||||
pub const DC_VC_AUTH_REQUIRED: i32 = 2;
|
||||
pub const DC_VC_CONTACT_CONFIRM: i32 = 6;
|
||||
pub const DC_BOB_ERROR: i32 = 0;
|
||||
pub const DC_BOB_SUCCESS: i32 = 1;
|
||||
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
|
||||
#[repr(i32)]
|
||||
pub enum Viewtype {
|
||||
|
||||
@@ -103,7 +103,7 @@ pub unsafe fn dc_imex_has_backup(
|
||||
pub unsafe fn dc_initiate_key_transfer(context: &Context) -> *mut libc::c_char {
|
||||
let mut setup_file_name: *mut libc::c_char = ptr::null_mut();
|
||||
let mut msg: Message;
|
||||
if dc_alloc_ongoing(context) == 0 {
|
||||
if !dc_alloc_ongoing(context) {
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
let setup_code = dc_create_setup_code(context);
|
||||
@@ -502,11 +502,9 @@ pub unsafe fn dc_normalize_setup_code(
|
||||
pub unsafe fn dc_job_do_DC_JOB_IMEX_IMAP(context: &Context, job: &Job) {
|
||||
let mut ok_to_continue = true;
|
||||
let mut success: libc::c_int = 0;
|
||||
let mut ongoing_allocated_here: libc::c_int = 0;
|
||||
let what: libc::c_int;
|
||||
|
||||
if !(0 == dc_alloc_ongoing(context)) {
|
||||
ongoing_allocated_here = 1;
|
||||
if dc_alloc_ongoing(context) {
|
||||
what = job.param.get_int(Param::Cmd).unwrap_or_default();
|
||||
let param1_s = job.param.get(Param::Arg).unwrap_or_default();
|
||||
let param1 = CString::yolo(param1_s);
|
||||
@@ -564,9 +562,6 @@ pub unsafe fn dc_job_do_DC_JOB_IMEX_IMAP(context: &Context, job: &Job) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if 0 != ongoing_allocated_here {
|
||||
dc_free_ongoing(context);
|
||||
}
|
||||
context.call_cb(
|
||||
|
||||
@@ -15,7 +15,6 @@ use crate::constants::*;
|
||||
use crate::contact::*;
|
||||
use crate::context::{do_heuristics_moves, Context};
|
||||
use crate::dc_mimeparser::*;
|
||||
use crate::dc_securejoin::*;
|
||||
use crate::dc_strencode::*;
|
||||
use crate::dc_tools::*;
|
||||
use crate::error::Result;
|
||||
@@ -24,6 +23,7 @@ use crate::location;
|
||||
use crate::message::*;
|
||||
use crate::param::*;
|
||||
use crate::peerstate::*;
|
||||
use crate::securejoin::handle_securejoin_handshake;
|
||||
use crate::sql;
|
||||
use crate::stock::StockMessage;
|
||||
use crate::types::*;
|
||||
@@ -400,7 +400,7 @@ unsafe fn add_parts(
|
||||
msgrmsg = 1;
|
||||
*chat_id = 0;
|
||||
allow_creation = 1;
|
||||
let handshake = dc_handle_securejoin_handshake(context, mime_parser, *from_id);
|
||||
let handshake = handle_securejoin_handshake(context, mime_parser, *from_id);
|
||||
if 0 != handshake & DC_HANDSHAKE_STOP_NORMAL_PROCESSING {
|
||||
*hidden = 1;
|
||||
*add_delete_job = handshake & DC_HANDSHAKE_ADD_DELETE_JOB;
|
||||
@@ -1034,7 +1034,6 @@ unsafe fn create_or_lookup_group(
|
||||
let group_explicitly_left: bool;
|
||||
let mut chat_id = 0;
|
||||
let mut chat_id_blocked = Blocked::Not;
|
||||
let mut chat_id_verified = 0;
|
||||
let mut grpid = "".to_string();
|
||||
let mut grpname = std::ptr::null_mut();
|
||||
let to_ids_cnt = to_ids.len();
|
||||
@@ -1216,14 +1215,9 @@ unsafe fn create_or_lookup_group(
|
||||
set_better_msg(mime_parser, &better_msg);
|
||||
|
||||
// check, if we have a chat with this group ID
|
||||
chat_id = chat::get_chat_id_by_grpid(
|
||||
context,
|
||||
&grpid,
|
||||
Some(&mut chat_id_blocked),
|
||||
&mut chat_id_verified,
|
||||
);
|
||||
let (mut chat_id, chat_id_verified, _blocked) = chat::get_chat_id_by_grpid(context, &grpid);
|
||||
if chat_id != 0 {
|
||||
if 0 != chat_id_verified
|
||||
if chat_id_verified
|
||||
&& 0 == check_verified_properties(
|
||||
context,
|
||||
mime_parser,
|
||||
|
||||
1010
src/dc_securejoin.rs
1010
src/dc_securejoin.rs
File diff suppressed because it is too large
Load Diff
@@ -10,20 +10,17 @@ pub const DC_TOKEN_INVITENUMBER: dc_tokennamespc_t = 100;
|
||||
|
||||
// Functions to read/write token from/to the database. A token is any string associated with a key.
|
||||
|
||||
pub fn dc_token_save(
|
||||
context: &Context,
|
||||
namespc: dc_tokennamespc_t,
|
||||
foreign_id: u32,
|
||||
token: &str,
|
||||
) -> bool {
|
||||
pub fn dc_token_save(context: &Context, namespc: dc_tokennamespc_t, foreign_id: u32) -> String {
|
||||
// foreign_id may be 0
|
||||
let token = dc_create_id();
|
||||
sql::execute(
|
||||
context,
|
||||
&context.sql,
|
||||
"INSERT INTO tokens (namespc, foreign_id, token, timestamp) VALUES (?, ?, ?, ?);",
|
||||
params![namespc as i32, foreign_id as i32, token, time()],
|
||||
params![namespc as i32, foreign_id as i32, &token, time()],
|
||||
)
|
||||
.is_ok()
|
||||
.ok();
|
||||
token
|
||||
}
|
||||
|
||||
pub fn dc_token_lookup(
|
||||
@@ -39,6 +36,15 @@ pub fn dc_token_lookup(
|
||||
)
|
||||
}
|
||||
|
||||
pub fn dc_token_lookup_or_new(
|
||||
context: &Context,
|
||||
namespc: dc_tokennamespc_t,
|
||||
foreign_id: u32,
|
||||
) -> String {
|
||||
dc_token_lookup(context, namespc, foreign_id)
|
||||
.unwrap_or_else(|| dc_token_save(context, namespc, foreign_id))
|
||||
}
|
||||
|
||||
pub fn dc_token_exists(context: &Context, namespc: dc_tokennamespc_t, token: &str) -> bool {
|
||||
context
|
||||
.sql
|
||||
|
||||
@@ -23,13 +23,13 @@ use crate::aheader::*;
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::dc_mimeparser::*;
|
||||
use crate::dc_securejoin::*;
|
||||
use crate::dc_tools::*;
|
||||
use crate::error::*;
|
||||
use crate::key::*;
|
||||
use crate::keyring::*;
|
||||
use crate::peerstate::*;
|
||||
use crate::pgp::*;
|
||||
use crate::securejoin::handle_degrade_event;
|
||||
use crate::types::*;
|
||||
use crate::x::*;
|
||||
|
||||
@@ -440,7 +440,7 @@ impl E2eeHelper {
|
||||
}
|
||||
if let Some(ref peerstate) = peerstate {
|
||||
if peerstate.degrade_event.is_some() {
|
||||
dc_handle_degrade_event(context, &peerstate);
|
||||
handle_degrade_event(context, &peerstate);
|
||||
}
|
||||
if let Some(ref key) = peerstate.gossip_key {
|
||||
public_keyring_for_validate.add_ref(key);
|
||||
@@ -681,7 +681,7 @@ unsafe fn update_gossip_peerstates(
|
||||
}
|
||||
if let Some(peerstate) = peerstate {
|
||||
if peerstate.degrade_event.is_some() {
|
||||
dc_handle_degrade_event(context, &peerstate);
|
||||
handle_degrade_event(context, &peerstate);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,11 +57,11 @@ mod dc_loginparam;
|
||||
mod dc_mimefactory;
|
||||
pub mod dc_mimeparser;
|
||||
pub mod dc_receive_imf;
|
||||
pub mod dc_securejoin;
|
||||
mod dc_simplify;
|
||||
mod dc_strencode;
|
||||
mod dc_token;
|
||||
pub mod dc_tools;
|
||||
pub mod securejoin;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_utils;
|
||||
|
||||
@@ -220,7 +220,7 @@ pub fn send_locations_to_chat(context: &Context, chat_id: u32, seconds: i64) {
|
||||
msg.text =
|
||||
Some(context.stock_system_msg(StockMessage::MsgLocationEnabled, "", "", 0));
|
||||
msg.param.set_int(Param::Cmd, 8);
|
||||
unsafe { chat::send_msg(context, chat_id, &mut msg).unwrap() };
|
||||
chat::send_msg(context, chat_id, &mut msg).unwrap();
|
||||
} else if 0 == seconds && is_sending_locations_before {
|
||||
let stock_str =
|
||||
context.stock_system_msg(StockMessage::MsgLocationDisabled, "", "", 0);
|
||||
@@ -607,7 +607,7 @@ pub fn job_do_DC_JOB_MAYBE_SEND_LOCATIONS(context: &Context, _job: &Job) {
|
||||
msg.hidden = true;
|
||||
msg.param.set_int(Param::Cmd, 9);
|
||||
// TODO: handle cleanup on error
|
||||
unsafe { chat::send_msg(context, chat_id as u32, &mut msg).unwrap() };
|
||||
chat::send_msg(context, chat_id as u32, &mut msg).unwrap();
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
|
||||
@@ -57,3 +57,10 @@ macro_rules! log_event {
|
||||
formatted_c.as_ptr() as libc::uintptr_t);
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! emit_event {
|
||||
($ctx:expr, $event:expr, $data1:expr, $data2:expr) => {
|
||||
$ctx.call_cb($event, $data1 as libc::uintptr_t, $data2 as libc::uintptr_t);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -178,8 +178,11 @@ impl<'a> Peerstate<'a> {
|
||||
OR gossip_key_fingerprint=? COLLATE NOCASE \
|
||||
ORDER BY public_key_fingerprint=? DESC;";
|
||||
|
||||
let fp = fingerprint.as_bytes();
|
||||
Self::from_stmt(context, query, params![fp, fp, fp])
|
||||
Self::from_stmt(
|
||||
context,
|
||||
query,
|
||||
params![fingerprint, fingerprint, fingerprint],
|
||||
)
|
||||
}
|
||||
|
||||
fn from_stmt<P>(context: &'a Context, query: &str, params: P) -> Option<Self>
|
||||
@@ -523,6 +526,10 @@ mod tests {
|
||||
// clear to_save, as that is not persissted
|
||||
peerstate.to_save = None;
|
||||
assert_eq!(peerstate, peerstate_new);
|
||||
let peerstate_new2 =
|
||||
Peerstate::from_fingerprint(&ctx.ctx, &ctx.ctx.sql, &pub_key.fingerprint())
|
||||
.expect("failed to load peerstate from db");
|
||||
assert_eq!(peerstate, peerstate_new2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
782
src/securejoin.rs
Normal file
782
src/securejoin.rs
Normal file
@@ -0,0 +1,782 @@
|
||||
use mmime::mailimf_types::*;
|
||||
use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
|
||||
use std::ptr;
|
||||
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::chat::{self, Chat};
|
||||
use crate::configure::*;
|
||||
use crate::constants::*;
|
||||
use crate::contact::*;
|
||||
use crate::context::Context;
|
||||
use crate::dc_mimeparser::*;
|
||||
use crate::dc_token::*;
|
||||
use crate::dc_tools::*;
|
||||
use crate::e2ee::*;
|
||||
use crate::error::Error;
|
||||
use crate::key::*;
|
||||
use crate::lot::LotState;
|
||||
use crate::message::*;
|
||||
use crate::param::*;
|
||||
use crate::peerstate::*;
|
||||
use crate::qr::check_qr;
|
||||
use crate::stock::StockMessage;
|
||||
use crate::types::*;
|
||||
|
||||
pub const NON_ALPHANUMERIC_WITHOUT_DOT: &AsciiSet = &NON_ALPHANUMERIC.remove(b'.');
|
||||
|
||||
macro_rules! progress {
|
||||
($context:tt, $event:expr, $contact_id:expr, $progress:expr) => {
|
||||
assert!(
|
||||
$progress >= 0 && $progress <= 1000,
|
||||
"value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success"
|
||||
);
|
||||
$context.call_cb($event, $contact_id as uintptr_t, $progress as uintptr_t);
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! joiner_progress {
|
||||
($context:tt, $contact_id:expr, $progress:expr) => {
|
||||
progress!(
|
||||
$context,
|
||||
Event::SECUREJOIN_JOINER_PROGRESS,
|
||||
$contact_id,
|
||||
$progress
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! inviter_progress {
|
||||
($context:tt, $contact_id:expr, $progress:expr) => {
|
||||
progress!(
|
||||
$context,
|
||||
Event::SECUREJOIN_INVITER_PROGRESS,
|
||||
$contact_id,
|
||||
$progress
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! get_qr_attr {
|
||||
($context:tt, $attr:ident) => {
|
||||
$context
|
||||
.bob
|
||||
.read()
|
||||
.unwrap()
|
||||
.qr_scan
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.$attr
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
};
|
||||
}
|
||||
|
||||
pub fn dc_get_securejoin_qr(context: &Context, group_chat_id: uint32_t) -> Option<String> {
|
||||
/* =========================================================
|
||||
==== Alice - the inviter side ====
|
||||
==== Step 1 in "Setup verified contact" protocol ====
|
||||
========================================================= */
|
||||
|
||||
let fingerprint: String;
|
||||
|
||||
ensure_secret_key_exists(context).ok();
|
||||
let invitenumber = dc_token_lookup_or_new(context, DC_TOKEN_INVITENUMBER, group_chat_id);
|
||||
let auth = dc_token_lookup_or_new(context, DC_TOKEN_AUTH, group_chat_id);
|
||||
let self_addr = match context.sql.get_config(context, "configured_addr") {
|
||||
Some(addr) => addr,
|
||||
None => {
|
||||
error!(context, 0, "Not configured, cannot generate QR code.",);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let self_name = context
|
||||
.sql
|
||||
.get_config(context, "displayname")
|
||||
.unwrap_or_default();
|
||||
|
||||
fingerprint = match get_self_fingerprint(context) {
|
||||
Some(fp) => fp,
|
||||
None => {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let self_addr_urlencoded =
|
||||
utf8_percent_encode(&self_addr, NON_ALPHANUMERIC_WITHOUT_DOT).to_string();
|
||||
let self_name_urlencoded =
|
||||
utf8_percent_encode(&self_name, NON_ALPHANUMERIC_WITHOUT_DOT).to_string();
|
||||
|
||||
let qr = if 0 != group_chat_id {
|
||||
if let Ok(chat) = Chat::load_from_db(context, group_chat_id) {
|
||||
let group_name = chat.get_name();
|
||||
let group_name_urlencoded =
|
||||
utf8_percent_encode(&group_name, NON_ALPHANUMERIC).to_string();
|
||||
|
||||
Some(format!(
|
||||
"OPENPGP4FPR:{}#a={}&g={}&x={}&i={}&s={}",
|
||||
fingerprint,
|
||||
self_addr_urlencoded,
|
||||
&group_name_urlencoded,
|
||||
&chat.grpid,
|
||||
&invitenumber,
|
||||
&auth,
|
||||
))
|
||||
} else {
|
||||
error!(
|
||||
context,
|
||||
0, "Cannot get QR-code for chat-id {}", group_chat_id,
|
||||
);
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
Some(format!(
|
||||
"OPENPGP4FPR:{}#a={}&n={}&i={}&s={}",
|
||||
fingerprint, self_addr_urlencoded, self_name_urlencoded, &invitenumber, &auth,
|
||||
))
|
||||
};
|
||||
|
||||
info!(context, 0, "Generated QR code: {}", qr.as_ref().unwrap());
|
||||
|
||||
qr
|
||||
}
|
||||
|
||||
fn get_self_fingerprint(context: &Context) -> Option<String> {
|
||||
if let Some(self_addr) = context.sql.get_config(context, "configured_addr") {
|
||||
if let Some(key) = Key::from_self_public(context, self_addr, &context.sql) {
|
||||
return Some(key.fingerprint());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn dc_join_securejoin(context: &Context, qr: &str) -> uint32_t {
|
||||
let cleanup =
|
||||
|context: &Context, contact_chat_id: u32, ongoing_allocated: bool, join_vg: bool| {
|
||||
let mut bob = context.bob.write().unwrap();
|
||||
bob.expects = 0;
|
||||
let ret_chat_id = if bob.status == DC_BOB_SUCCESS {
|
||||
if join_vg {
|
||||
chat::get_chat_id_by_grpid(
|
||||
context,
|
||||
bob.qr_scan.as_ref().unwrap().text2.as_ref().unwrap(),
|
||||
)
|
||||
.0
|
||||
} else {
|
||||
contact_chat_id
|
||||
}
|
||||
} else {
|
||||
0
|
||||
};
|
||||
bob.qr_scan = None;
|
||||
|
||||
if ongoing_allocated {
|
||||
dc_free_ongoing(context);
|
||||
}
|
||||
ret_chat_id as uint32_t
|
||||
};
|
||||
/* ==========================================================
|
||||
==== Bob - the joiner's side =====
|
||||
==== Step 2 in "Setup verified contact" protocol =====
|
||||
========================================================== */
|
||||
let mut contact_chat_id: uint32_t = 0;
|
||||
let mut join_vg: bool = false;
|
||||
|
||||
info!(context, 0, "Requesting secure-join ...",);
|
||||
ensure_secret_key_exists(context).ok();
|
||||
if !dc_alloc_ongoing(context) {
|
||||
return cleanup(&context, contact_chat_id, false, join_vg);
|
||||
}
|
||||
let qr_scan = check_qr(context, &qr);
|
||||
if qr_scan.state != LotState::QrAskVerifyContact && qr_scan.state != LotState::QrAskVerifyGroup
|
||||
{
|
||||
error!(context, 0, "Unknown QR code.",);
|
||||
return cleanup(&context, contact_chat_id, true, join_vg);
|
||||
}
|
||||
contact_chat_id = chat::create_by_contact_id(context, qr_scan.id).unwrap_or_default();
|
||||
if contact_chat_id == 0 {
|
||||
error!(context, 0, "Unknown contact.",);
|
||||
return cleanup(&context, contact_chat_id, true, join_vg);
|
||||
}
|
||||
if check_exit(context) {
|
||||
return cleanup(&context, contact_chat_id, true, join_vg);
|
||||
}
|
||||
join_vg = qr_scan.get_state() == LotState::QrAskVerifyGroup;
|
||||
{
|
||||
let mut bob = context.bob.write().unwrap();
|
||||
bob.status = 0;
|
||||
bob.qr_scan = Some(qr_scan);
|
||||
}
|
||||
if fingerprint_equals_sender(
|
||||
context,
|
||||
context
|
||||
.bob
|
||||
.read()
|
||||
.unwrap()
|
||||
.qr_scan
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.fingerprint
|
||||
.as_ref()
|
||||
.unwrap(),
|
||||
contact_chat_id,
|
||||
) {
|
||||
info!(context, 0, "Taking protocol shortcut.");
|
||||
context.bob.write().unwrap().expects = DC_VC_CONTACT_CONFIRM;
|
||||
joiner_progress!(context, chat_id_2_contact_id(context, contact_chat_id), 400);
|
||||
let own_fingerprint = get_self_fingerprint(context).unwrap();
|
||||
send_handshake_msg(
|
||||
context,
|
||||
contact_chat_id,
|
||||
if join_vg {
|
||||
"vg-request-with-auth"
|
||||
} else {
|
||||
"vc-request-with-auth"
|
||||
},
|
||||
get_qr_attr!(context, auth).to_string(),
|
||||
Some(own_fingerprint),
|
||||
if join_vg {
|
||||
get_qr_attr!(context, text2).to_string()
|
||||
} else {
|
||||
"".to_string()
|
||||
},
|
||||
);
|
||||
} else {
|
||||
context.bob.write().unwrap().expects = DC_VC_AUTH_REQUIRED;
|
||||
send_handshake_msg(
|
||||
context,
|
||||
contact_chat_id,
|
||||
if join_vg { "vg-request" } else { "vc-request" },
|
||||
get_qr_attr!(context, invitenumber),
|
||||
None,
|
||||
"",
|
||||
);
|
||||
}
|
||||
|
||||
// Bob -> Alice
|
||||
while !check_exit(&context) {
|
||||
std::thread::sleep(std::time::Duration::new(0, 3_000_000));
|
||||
}
|
||||
cleanup(&context, contact_chat_id, true, join_vg)
|
||||
}
|
||||
|
||||
fn check_exit(context: &Context) -> bool {
|
||||
context
|
||||
.running_state
|
||||
.clone()
|
||||
.read()
|
||||
.unwrap()
|
||||
.shall_stop_ongoing
|
||||
}
|
||||
|
||||
fn send_handshake_msg(
|
||||
context: &Context,
|
||||
contact_chat_id: uint32_t,
|
||||
step: &str,
|
||||
param2: impl AsRef<str>,
|
||||
fingerprint: Option<String>,
|
||||
grpid: impl AsRef<str>,
|
||||
) {
|
||||
let mut msg = unsafe { dc_msg_new_untyped(context) };
|
||||
msg.type_0 = Viewtype::Text;
|
||||
msg.text = Some(format!("Secure-Join: {}", step));
|
||||
msg.hidden = true;
|
||||
msg.param.set_int(Param::Cmd, 7);
|
||||
if step.is_empty() {
|
||||
msg.param.remove(Param::Arg);
|
||||
} else {
|
||||
msg.param.set(Param::Arg, step);
|
||||
}
|
||||
if !param2.as_ref().is_empty() {
|
||||
msg.param.set(Param::Arg2, param2);
|
||||
}
|
||||
if let Some(fp) = fingerprint {
|
||||
msg.param.set(Param::Arg3, fp);
|
||||
}
|
||||
if !grpid.as_ref().is_empty() {
|
||||
msg.param.set(Param::Arg4, grpid.as_ref());
|
||||
}
|
||||
if step == "vg-request" || step == "vc-request" {
|
||||
msg.param.set_int(
|
||||
Param::ForcePlaintext,
|
||||
ForcePlaintext::AddAutocryptHeader as i32,
|
||||
);
|
||||
} else {
|
||||
msg.param.set_int(Param::GuranteeE2ee, 1);
|
||||
}
|
||||
// TODO. handle cleanup on error
|
||||
chat::send_msg(context, contact_chat_id, &mut msg).unwrap();
|
||||
}
|
||||
|
||||
fn chat_id_2_contact_id(context: &Context, contact_chat_id: uint32_t) -> uint32_t {
|
||||
let contacts = chat::get_chat_contacts(context, contact_chat_id);
|
||||
if contacts.len() == 1 {
|
||||
contacts[0]
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
fn fingerprint_equals_sender(
|
||||
context: &Context,
|
||||
fingerprint: impl AsRef<str>,
|
||||
contact_chat_id: u32,
|
||||
) -> bool {
|
||||
let contacts = chat::get_chat_contacts(context, contact_chat_id);
|
||||
|
||||
if contacts.len() == 1 {
|
||||
if let Ok(contact) = Contact::load_from_db(context, contacts[0]) {
|
||||
if let Some(peerstate) = Peerstate::from_addr(context, &context.sql, contact.get_addr())
|
||||
{
|
||||
let fingerprint_normalized = dc_normalize_fingerprint(fingerprint.as_ref());
|
||||
if peerstate.public_key_fingerprint.is_some()
|
||||
&& &fingerprint_normalized == peerstate.public_key_fingerprint.as_ref().unwrap()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/* library private: secure-join */
|
||||
pub fn handle_securejoin_handshake(
|
||||
context: &Context,
|
||||
mimeparser: &dc_mimeparser_t,
|
||||
contact_id: uint32_t,
|
||||
) -> libc::c_int {
|
||||
let own_fingerprint: String;
|
||||
|
||||
if contact_id <= DC_CONTACT_ID_LAST_SPECIAL {
|
||||
return 0;
|
||||
}
|
||||
let step = match lookup_field(mimeparser, "Secure-Join") {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
info!(
|
||||
context,
|
||||
0, ">>>>>>>>>>>>>>>>>>>>>>>>> secure-join message \'{}\' received", step,
|
||||
);
|
||||
let (contact_chat_id, contact_chat_id_blocked) =
|
||||
chat::create_or_lookup_by_contact_id(context, contact_id, Blocked::Not).unwrap_or_default();
|
||||
|
||||
if contact_chat_id_blocked != Blocked::Not {
|
||||
chat::unblock(context, contact_chat_id);
|
||||
}
|
||||
let mut ret: libc::c_int = DC_HANDSHAKE_STOP_NORMAL_PROCESSING;
|
||||
let join_vg = step.starts_with("vg-");
|
||||
|
||||
match step.as_str() {
|
||||
"vg-request" | "vc-request" => {
|
||||
/* =========================================================
|
||||
==== Alice - the inviter side ====
|
||||
==== Step 3 in "Setup verified contact" protocol ====
|
||||
========================================================= */
|
||||
// this message may be unencrypted (Bob, the joinder and the sender, might not have Alice's key yet)
|
||||
// it just ensures, we have Bobs key now. If we do _not_ have the key because eg. MitM has removed it,
|
||||
// send_message() will fail with the error "End-to-end-encryption unavailable unexpectedly.", so, there is no additional check needed here.
|
||||
// verify that the `Secure-Join-Invitenumber:`-header matches invitenumber written to the QR code
|
||||
let invitenumber = match lookup_field(mimeparser, "Secure-Join-Invitenumber") {
|
||||
Some(n) => n,
|
||||
None => {
|
||||
warn!(context, 0, "Secure-join denied (invitenumber missing).",);
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
if !dc_token_exists(context, DC_TOKEN_INVITENUMBER, &invitenumber) {
|
||||
warn!(context, 0, "Secure-join denied (bad invitenumber).",);
|
||||
return ret;
|
||||
}
|
||||
info!(context, 0, "Secure-join requested.",);
|
||||
|
||||
inviter_progress!(context, contact_id, 300);
|
||||
send_handshake_msg(
|
||||
context,
|
||||
contact_chat_id,
|
||||
&format!("{}-auth-required", &step[..2]),
|
||||
"",
|
||||
None,
|
||||
"",
|
||||
);
|
||||
}
|
||||
"vg-auth-required" | "vc-auth-required" => {
|
||||
let cond = {
|
||||
let bob = context.bob.read().unwrap();
|
||||
let scan = bob.qr_scan.as_ref();
|
||||
scan.is_none()
|
||||
|| bob.expects != DC_VC_AUTH_REQUIRED
|
||||
|| join_vg && scan.unwrap().state != LotState::QrAskVerifyGroup
|
||||
};
|
||||
|
||||
if cond {
|
||||
warn!(context, 0, "auth-required message out of sync.",);
|
||||
// no error, just aborted somehow or a mail from another handshake
|
||||
return ret;
|
||||
}
|
||||
let scanned_fingerprint_of_alice = get_qr_attr!(context, fingerprint).to_string();
|
||||
let auth = get_qr_attr!(context, auth).to_string();
|
||||
|
||||
if !encrypted_and_signed(mimeparser, &scanned_fingerprint_of_alice) {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_chat_id,
|
||||
if mimeparser.e2ee_helper.encrypted {
|
||||
"No valid signature."
|
||||
} else {
|
||||
"Not encrypted."
|
||||
},
|
||||
);
|
||||
end_bobs_joining(context, DC_BOB_ERROR);
|
||||
return ret;
|
||||
}
|
||||
if !fingerprint_equals_sender(context, &scanned_fingerprint_of_alice, contact_chat_id) {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_chat_id,
|
||||
"Fingerprint mismatch on joiner-side.",
|
||||
);
|
||||
end_bobs_joining(context, DC_BOB_ERROR);
|
||||
return ret;
|
||||
}
|
||||
info!(context, 0, "Fingerprint verified.",);
|
||||
own_fingerprint = get_self_fingerprint(context).unwrap();
|
||||
joiner_progress!(context, contact_id, 400);
|
||||
context.bob.write().unwrap().expects = DC_VC_CONTACT_CONFIRM;
|
||||
|
||||
send_handshake_msg(
|
||||
context,
|
||||
contact_chat_id,
|
||||
&format!("{}-request-with-auth", &step[..2]),
|
||||
auth,
|
||||
Some(own_fingerprint),
|
||||
if join_vg {
|
||||
get_qr_attr!(context, text2).to_string()
|
||||
} else {
|
||||
"".to_string()
|
||||
},
|
||||
);
|
||||
}
|
||||
"vg-request-with-auth" | "vc-request-with-auth" => {
|
||||
/* ============================================================
|
||||
==== Alice - the inviter side ====
|
||||
==== Steps 5+6 in "Setup verified contact" protocol ====
|
||||
==== Step 6 in "Out-of-band verified groups" protocol ====
|
||||
============================================================ */
|
||||
// verify that Secure-Join-Fingerprint:-header matches the fingerprint of Bob
|
||||
let fingerprint = match lookup_field(mimeparser, "Secure-Join-Fingerprint") {
|
||||
Some(fp) => fp,
|
||||
None => {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_chat_id,
|
||||
"Fingerprint not provided.",
|
||||
);
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
if !encrypted_and_signed(mimeparser, &fingerprint) {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_chat_id,
|
||||
"Auth not encrypted.",
|
||||
);
|
||||
return ret;
|
||||
}
|
||||
if !fingerprint_equals_sender(context, &fingerprint, contact_chat_id) {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_chat_id,
|
||||
"Fingerprint mismatch on inviter-side.",
|
||||
);
|
||||
return ret;
|
||||
}
|
||||
info!(context, 0, "Fingerprint verified.",);
|
||||
// verify that the `Secure-Join-Auth:`-header matches the secret written to the QR code
|
||||
let auth_0 = match lookup_field(mimeparser, "Secure-Join-Auth") {
|
||||
Some(auth) => auth,
|
||||
None => {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_chat_id,
|
||||
"Auth not provided.",
|
||||
);
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
if !dc_token_exists(context, DC_TOKEN_AUTH, &auth_0) {
|
||||
could_not_establish_secure_connection(context, contact_chat_id, "Auth invalid.");
|
||||
return ret;
|
||||
}
|
||||
if mark_peer_as_verified(context, fingerprint).is_err() {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_chat_id,
|
||||
"Fingerprint mismatch on inviter-side.",
|
||||
);
|
||||
return ret;
|
||||
}
|
||||
Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinInvited);
|
||||
info!(context, 0, "Auth verified.",);
|
||||
secure_connection_established(context, contact_chat_id);
|
||||
emit_event!(context, Event::CONTACTS_CHANGED, contact_id, 0);
|
||||
inviter_progress!(context, contact_id, 600);
|
||||
if join_vg {
|
||||
let field_grpid = lookup_field(mimeparser, "Secure-Join-Group").unwrap_or_default();
|
||||
let (group_chat_id, _, _) = chat::get_chat_id_by_grpid(context, &field_grpid);
|
||||
if group_chat_id == 0 {
|
||||
error!(context, 0, "Chat {} not found.", &field_grpid);
|
||||
return ret;
|
||||
} else {
|
||||
chat::add_contact_to_chat_ex(context, group_chat_id, contact_id, 0x1i32);
|
||||
}
|
||||
} else {
|
||||
send_handshake_msg(context, contact_chat_id, "vc-contact-confirm", "", None, "");
|
||||
inviter_progress!(context, contact_id, 1000);
|
||||
}
|
||||
}
|
||||
"vg-member-added" | "vc-contact-confirm" => {
|
||||
if join_vg {
|
||||
ret = DC_HANDSHAKE_CONTINUE_NORMAL_PROCESSING;
|
||||
}
|
||||
if context.bob.read().unwrap().expects != DC_VC_CONTACT_CONFIRM {
|
||||
info!(context, 0, "Message belongs to a different handshake.",);
|
||||
return ret;
|
||||
}
|
||||
let cond = {
|
||||
let bob = context.bob.read().unwrap();
|
||||
let scan = bob.qr_scan.as_ref();
|
||||
scan.is_none() || join_vg && scan.unwrap().state != LotState::QrAskVerifyGroup
|
||||
};
|
||||
if cond {
|
||||
warn!(
|
||||
context,
|
||||
0, "Message out of sync or belongs to a different handshake.",
|
||||
);
|
||||
return ret;
|
||||
}
|
||||
let scanned_fingerprint_of_alice = get_qr_attr!(context, fingerprint).to_string();
|
||||
|
||||
let vg_expect_encrypted = if join_vg {
|
||||
let group_id = get_qr_attr!(context, text2).to_string();
|
||||
let (_, is_verified_group, _) = chat::get_chat_id_by_grpid(context, group_id);
|
||||
// when joining a non-verified group
|
||||
// the vg-member-added message may be unencrypted
|
||||
// when not all group members have keys or prefer encryption.
|
||||
// So only expect encryption if this is a verified group
|
||||
is_verified_group
|
||||
} else {
|
||||
// setup contact is always encrypted
|
||||
true
|
||||
};
|
||||
if vg_expect_encrypted
|
||||
&& !encrypted_and_signed(mimeparser, &scanned_fingerprint_of_alice)
|
||||
{
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_chat_id,
|
||||
"Contact confirm message not encrypted.",
|
||||
);
|
||||
end_bobs_joining(context, DC_BOB_ERROR);
|
||||
return ret;
|
||||
}
|
||||
|
||||
if mark_peer_as_verified(context, &scanned_fingerprint_of_alice).is_err() {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_chat_id,
|
||||
"Fingerprint mismatch on joiner-side.",
|
||||
);
|
||||
return ret;
|
||||
}
|
||||
Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinJoined);
|
||||
emit_event!(context, Event::CONTACTS_CHANGED, 0, 0);
|
||||
let cg_member_added =
|
||||
lookup_field(mimeparser, "Chat-Group-Member-Added").unwrap_or_default();
|
||||
if join_vg && !addr_equals_self(context, cg_member_added) {
|
||||
info!(context, 0, "Message belongs to a different handshake (scaled up contact anyway to allow creation of group).");
|
||||
return ret;
|
||||
}
|
||||
secure_connection_established(context, contact_chat_id);
|
||||
context.bob.write().unwrap().expects = 0;
|
||||
if join_vg {
|
||||
send_handshake_msg(
|
||||
context,
|
||||
contact_chat_id,
|
||||
"vg-member-added-received",
|
||||
"",
|
||||
None,
|
||||
"",
|
||||
);
|
||||
}
|
||||
end_bobs_joining(context, DC_BOB_SUCCESS);
|
||||
}
|
||||
"vg-member-added-received" => {
|
||||
/* ============================================================
|
||||
==== Alice - the inviter side ====
|
||||
==== Step 8 in "Out-of-band verified groups" protocol ====
|
||||
============================================================ */
|
||||
if let Ok(contact) = Contact::get_by_id(context, contact_id) {
|
||||
if contact.is_verified() == VerifiedStatus::Unverified {
|
||||
warn!(context, 0, "vg-member-added-received invalid.",);
|
||||
return ret;
|
||||
}
|
||||
inviter_progress!(context, contact_id, 800);
|
||||
inviter_progress!(context, contact_id, 1000);
|
||||
} else {
|
||||
warn!(context, 0, "vg-member-added-received invalid.",);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
warn!(context, 0, "invalid step: {}", step);
|
||||
}
|
||||
}
|
||||
if ret == DC_HANDSHAKE_STOP_NORMAL_PROCESSING {
|
||||
ret |= DC_HANDSHAKE_ADD_DELETE_JOB;
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
fn end_bobs_joining(context: &Context, status: libc::c_int) {
|
||||
context.bob.write().unwrap().status = status;
|
||||
dc_stop_ongoing_process(context);
|
||||
}
|
||||
|
||||
fn secure_connection_established(context: &Context, contact_chat_id: uint32_t) {
|
||||
let contact_id: uint32_t = chat_id_2_contact_id(context, contact_chat_id);
|
||||
let contact = Contact::get_by_id(context, contact_id);
|
||||
let addr = if let Ok(ref contact) = contact {
|
||||
contact.get_addr()
|
||||
} else {
|
||||
"?"
|
||||
};
|
||||
let msg = context.stock_string_repl_str(StockMessage::ContactVerified, addr);
|
||||
chat::add_device_msg(context, contact_chat_id, msg);
|
||||
emit_event!(context, Event::CHAT_MODIFIED, contact_chat_id, 0);
|
||||
}
|
||||
|
||||
fn lookup_field(mimeparser: &dc_mimeparser_t, key: &str) -> Option<String> {
|
||||
let field: *mut mailimf_field = dc_mimeparser_lookup_field(mimeparser, key);
|
||||
unsafe {
|
||||
let mut value: *const libc::c_char = ptr::null();
|
||||
if field.is_null()
|
||||
|| (*field).fld_type != MAILIMF_FIELD_OPTIONAL_FIELD as libc::c_int
|
||||
|| (*field).fld_data.fld_optional_field.is_null()
|
||||
|| {
|
||||
value = (*(*field).fld_data.fld_optional_field).fld_value;
|
||||
value.is_null()
|
||||
}
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(as_str(value).to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn could_not_establish_secure_connection(
|
||||
context: &Context,
|
||||
contact_chat_id: uint32_t,
|
||||
details: &str,
|
||||
) {
|
||||
let contact_id = chat_id_2_contact_id(context, contact_chat_id);
|
||||
let contact = Contact::get_by_id(context, contact_id);
|
||||
let msg = context.stock_string_repl_str(
|
||||
StockMessage::ContactNotVerified,
|
||||
if let Ok(ref contact) = contact {
|
||||
contact.get_addr()
|
||||
} else {
|
||||
"?"
|
||||
},
|
||||
);
|
||||
|
||||
chat::add_device_msg(context, contact_chat_id, &msg);
|
||||
error!(context, 0, "{} ({})", &msg, details);
|
||||
}
|
||||
|
||||
fn mark_peer_as_verified(context: &Context, fingerprint: impl AsRef<str>) -> Result<(), Error> {
|
||||
if let Some(ref mut peerstate) =
|
||||
Peerstate::from_fingerprint(context, &context.sql, fingerprint.as_ref())
|
||||
{
|
||||
if peerstate.set_verified(1, fingerprint.as_ref(), 2) {
|
||||
peerstate.prefer_encrypt = EncryptPreference::Mutual;
|
||||
peerstate.to_save = Some(ToSave::All);
|
||||
peerstate.save_to_db(&context.sql, false);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
bail!(
|
||||
"could not mark peer as verified for fingerprint {}",
|
||||
fingerprint.as_ref()
|
||||
);
|
||||
}
|
||||
|
||||
/* ******************************************************************************
|
||||
* Tools: Misc.
|
||||
******************************************************************************/
|
||||
|
||||
fn encrypted_and_signed(
|
||||
mimeparser: &dc_mimeparser_t,
|
||||
expected_fingerprint: impl AsRef<str>,
|
||||
) -> bool {
|
||||
if !mimeparser.e2ee_helper.encrypted {
|
||||
warn!(mimeparser.context, 0, "Message not encrypted.",);
|
||||
false
|
||||
} else if mimeparser.e2ee_helper.signatures.len() <= 0 {
|
||||
warn!(mimeparser.context, 0, "Message not signed.",);
|
||||
false
|
||||
} else if expected_fingerprint.as_ref().is_empty() {
|
||||
warn!(mimeparser.context, 0, "Fingerprint for comparison missing.",);
|
||||
false
|
||||
} else if !mimeparser
|
||||
.e2ee_helper
|
||||
.signatures
|
||||
.contains(expected_fingerprint.as_ref())
|
||||
{
|
||||
warn!(
|
||||
mimeparser.context,
|
||||
0,
|
||||
"Message does not match expected fingerprint {}.",
|
||||
expected_fingerprint.as_ref(),
|
||||
);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_degrade_event(context: &Context, peerstate: &Peerstate) {
|
||||
// - we do not issue an warning for DC_DE_ENCRYPTION_PAUSED as this is quite normal
|
||||
// - currently, we do not issue an extra warning for DC_DE_VERIFICATION_LOST - this always comes
|
||||
// together with DC_DE_FINGERPRINT_CHANGED which is logged, the idea is not to bother
|
||||
// with things they cannot fix, so the user is just kicked from the verified group
|
||||
// (and he will know this and can fix this)
|
||||
if Some(DegradeEvent::FingerprintChanged) == peerstate.degrade_event {
|
||||
let contact_id: i32 = context
|
||||
.sql
|
||||
.query_row_col(
|
||||
context,
|
||||
"SELECT id FROM contacts WHERE addr=?;",
|
||||
params![&peerstate.addr],
|
||||
0,
|
||||
)
|
||||
.unwrap_or_default();
|
||||
if contact_id > 0 {
|
||||
let (contact_chat_id, _) =
|
||||
chat::create_or_lookup_by_contact_id(context, contact_id as u32, Blocked::Deaddrop)
|
||||
.unwrap_or_default();
|
||||
|
||||
let peeraddr: &str = match peerstate.addr {
|
||||
Some(ref addr) => &addr,
|
||||
None => "",
|
||||
};
|
||||
let msg = context.stock_string_repl_str(StockMessage::ContactSetupChanged, peeraddr);
|
||||
|
||||
chat::add_device_msg(context, contact_chat_id, msg);
|
||||
emit_event!(context, Event::CHAT_MODIFIED, contact_chat_id, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user