diff --git a/python/install_py_bindings.sh b/python/install_py_bindings.sh index 1b5c71cf6..718229252 100755 --- a/python/install_py_bindings.sh +++ b/python/install_py_bindings.sh @@ -3,5 +3,5 @@ set -ex cargo build -p deltachat_ffi --release -rm -rf build/ +rm -rf build/ src/deltachat/*.so DCC_RS_DEV=`pwd`/.. pip install -e . diff --git a/python/src/deltachat/__init__.py b/python/src/deltachat/__init__.py index 7ad9d6052..624b58c69 100644 --- a/python/src/deltachat/__init__.py +++ b/python/src/deltachat/__init__.py @@ -2,7 +2,7 @@ from deltachat import capi, const from deltachat.capi import ffi from deltachat.account import Account # noqa -__version__ = "0.10.0.dev3" +__version__ = "0.10.0.dev4" _DC_CALLBACK_MAP = {} diff --git a/python/src/deltachat/account.py b/python/src/deltachat/account.py index a5404f04e..6fb32b73e 100644 --- a/python/src/deltachat/account.py +++ b/python/src/deltachat/account.py @@ -305,6 +305,19 @@ class Account(object): lib.dc_perform_imap_jobs(self._dc_context) self._imex_completed.wait() + def initiate_key_transfer(self): + """return setup code after a Autocrypt setup message + has been successfully sent to our own e-mail address ("self-sent message"). + If sending out was unsuccessful, a RuntimeError is raised. + """ + self.check_is_configured() + if not self._threads.is_started(): + raise RuntimeError("threads not running, can not send out") + res = lib.dc_initiate_key_transfer(self._dc_context) + if res == ffi.NULL: + raise RuntimeError("could not send out autocrypt setup message") + return from_dc_charpointer(res) + def start_threads(self): """ start IMAP/SMTP threads (and configure account if it hasn't happened). @@ -315,9 +328,10 @@ class Account(object): self.configure() self._threads.start() - def stop_threads(self): + def stop_threads(self, wait=True): """ stop IMAP/SMTP threads. """ - self._threads.stop(wait=True) + lib.dc_stop_ongoing_process(self._dc_context) + self._threads.stop(wait=wait) def _process_event(self, ctx, evt_name, data1, data2): assert ctx == self._dc_context @@ -362,14 +376,12 @@ class IOThreads: thread.join() def imap_thread_run(self): - print("starting imap thread") while not self._thread_quitflag: lib.dc_perform_imap_jobs(self._dc_context) lib.dc_perform_imap_fetch(self._dc_context) lib.dc_perform_imap_idle(self._dc_context) def smtp_thread_run(self): - print("starting smtp thread") while not self._thread_quitflag: lib.dc_perform_smtp_jobs(self._dc_context) lib.dc_perform_smtp_idle(self._dc_context) diff --git a/python/src/deltachat/message.py b/python/src/deltachat/message.py index 4719e0b45..87d4e9445 100644 --- a/python/src/deltachat/message.py +++ b/python/src/deltachat/message.py @@ -93,6 +93,14 @@ class Message(object): """ return MessageType(lib.dc_msg_get_viewtype(self._dc_msg)) + def is_setup_message(self): + """ return True if this message is a setup message. """ + return lib.dc_msg_is_setupmessage(self._dc_msg) + + def continue_key_transfer(self, setup_code): + """ extract key and use it as primary key for this account. """ + lib.dc_continue_key_transfer(self._dc_context, self.id, as_dc_charpointer(setup_code)) + @props.with_doc def time_sent(self): """UTC time when the message was sent. diff --git a/python/tests/conftest.py b/python/tests/conftest.py index ff1d4425c..64668d938 100644 --- a/python/tests/conftest.py +++ b/python/tests/conftest.py @@ -103,7 +103,18 @@ def acfactory(pytestconfig, tmpdir, request): ac._evlogger.set_timeout(30) ac.configure(**configdict) ac.start_threads() - self._finalizers.append(ac.stop_threads) + self._finalizers.append(lambda: ac.stop_threads(wait=False)) + return ac + + def clone_online_account(self, account): + self.live_count += 1 + tmpdb = tmpdir.join("livedb%d" % self.live_count) + ac = Account(tmpdb.strpath, logid="ac{}".format(self.live_count)) + ac._evlogger.init_time = self.init_time + ac._evlogger.set_timeout(30) + ac.configure(addr=account.get_config("addr"), mail_pw=account.get_config("mail_pw")) + ac.start_threads() + self._finalizers.append(lambda: ac.stop_threads(wait=False)) return ac return AccountMaker() diff --git a/python/tests/test_account.py b/python/tests/test_account.py index b1e4aa00b..b24ebb367 100644 --- a/python/tests/test_account.py +++ b/python/tests/test_account.py @@ -252,6 +252,11 @@ class TestOfflineAccount: assert messages[0].text == "msg1" assert os.path.exists(messages[1].filename) + def test_ac_setup_message_fails(self, acfactory): + ac1 = acfactory.get_configured_offline_account() + with pytest.raises(RuntimeError): + ac1.initiate_key_transfer() + class TestOnlineAccount: def test_one_account_init(self, acfactory): @@ -441,3 +446,22 @@ class TestOnlineAccount: messages = chat2.get_messages() assert len(messages) == 1 assert messages[0].text == "msg1" + + def test_ac_setup_message(self, acfactory): + # note that the receiving account needs to be configured and running + # before ther setup message is send. DC does not read old messages + # as of Jul2019 + ac1 = acfactory.get_online_configuring_account() + ac2 = acfactory.clone_online_account(ac1) + wait_configuration_progress(ac2, 1000) + wait_configuration_progress(ac1, 1000) + assert ac1.get_info()["fingerprint"] != ac2.get_info()["fingerprint"] + setup_code = ac1.initiate_key_transfer() + ac2._evlogger.set_timeout(10) + ev = ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED") + msg = ac2.get_message_by_id(ev[2]) + assert msg.is_setup_message() + print("*************** Incoming ASM File at: ", msg.filename) + print("*************** Setup Code: ", setup_code) + msg.continue_key_transfer(setup_code) + assert ac1.get_info()["fingerprint"] == ac2.get_info()["fingerprint"] diff --git a/src/dc_imex.rs b/src/dc_imex.rs index 22b81c891..172b7790d 100644 --- a/src/dc_imex.rs +++ b/src/dc_imex.rs @@ -1,4 +1,4 @@ -use std::ffi::{CStr, CString}; +use std::ffi::CString; use mmime::mailmime_content::*; use mmime::mmapstring::*; @@ -362,11 +362,7 @@ pub unsafe fn dc_continue_key_transfer( } || *filename.offset(0isize) as libc::c_int == 0i32 { - dc_log_error( - context, - 0i32, - b"Message is no Autocrypt Setup Message.\x00" as *const u8 as *const libc::c_char, - ); + error!(context, 0, "Message is no Autocrypt Setup Message.",); } else if 0 == dc_read_file( context, @@ -377,29 +373,15 @@ pub unsafe fn dc_continue_key_transfer( || filecontent.is_null() || filebytes <= 0 { - dc_log_error( - context, - 0i32, - b"Cannot read Autocrypt Setup Message file.\x00" as *const u8 - as *const libc::c_char, - ); + error!(context, 0, "Cannot read Autocrypt Setup Message file.",); } else { norm_sc = dc_normalize_setup_code(context, setup_code); if norm_sc.is_null() { - dc_log_warning( - context, - 0i32, - b"Cannot normalize Setup Code.\x00" as *const u8 as *const libc::c_char, - ); + warn!(context, 0, "Cannot normalize Setup Code.",); } else { armored_key = dc_decrypt_setup_file(context, norm_sc, filecontent); if armored_key.is_null() { - dc_log_warning( - context, - 0i32, - b"Cannot decrypt Autocrypt Setup Message.\x00" as *const u8 - as *const libc::c_char, - ); + warn!(context, 0, "Cannot decrypt Autocrypt Setup Message.",); } else if !(0 == set_self_key(context, armored_key, 1i32)) { /*set default*/ /* error already logged */ @@ -420,136 +402,96 @@ pub unsafe fn dc_continue_key_transfer( // TODO should return bool /rtn unsafe fn set_self_key( context: &Context, - armored: *const libc::c_char, + armored_c: *const libc::c_char, set_default: libc::c_int, ) -> libc::c_int { - let mut success: libc::c_int = 0i32; - let buf: *mut libc::c_char; - // pointer inside buf, MUST NOT be free()'d - let mut buf_headerline: *const libc::c_char = 0 as *const libc::c_char; - // - " - - let mut buf_preferencrypt: *const libc::c_char = 0 as *const libc::c_char; - // - " - - let mut buf_base64: *const libc::c_char = 0 as *const libc::c_char; + let mut success = 0; + let mut stmt: *mut sqlite3_stmt = 0 as *mut sqlite3_stmt; let mut self_addr: *mut libc::c_char = 0 as *mut libc::c_char; - buf = dc_strdup(armored); - if 0 == dc_split_armored_data( - buf, - &mut buf_headerline, - 0 as *mut *const libc::c_char, - &mut buf_preferencrypt, - &mut buf_base64, - ) || strcmp( - buf_headerline, - b"-----BEGIN PGP PRIVATE KEY BLOCK-----\x00" as *const u8 as *const libc::c_char, - ) != 0i32 - || buf_base64.is_null() + + assert!(!armored_c.is_null(), "invalid buffer"); + let armored = as_str(armored_c); + + if let Some((private_key, public_key, header)) = + Key::from_armored_string(armored, KeyType::Private) + .and_then(|(k, h)| if k.verify() { Some((k, h)) } else { None }) + .and_then(|(k, h)| k.split_key().map(|pub_key| (k, pub_key, h))) { - dc_log_warning( + stmt = dc_sqlite3_prepare( context, - 0i32, - b"File does not contain a private key.\x00" as *const u8 as *const libc::c_char, + &context.sql, + b"DELETE FROM keypairs WHERE public_key=? OR private_key=?;\x00" as *const u8 + as *const libc::c_char, ); - } else { - if let Some((private_key, public_key)) = Key::from_base64( - CStr::from_ptr(buf_base64).to_str().unwrap(), - KeyType::Private, - ) - .and_then(|k| if k.verify() { Some(k) } else { None }) - .and_then(|k| k.split_key().map(|pub_key| (k, pub_key))) - { - stmt = dc_sqlite3_prepare( + let pub_bytes = public_key.to_bytes(); + sqlite3_bind_blob( + stmt, + 1, + pub_bytes.as_ptr() as *const _, + pub_bytes.len() as libc::c_int, + None, + ); + let priv_bytes = private_key.to_bytes(); + sqlite3_bind_blob( + stmt, + 2, + priv_bytes.as_ptr() as *const _, + priv_bytes.len() as libc::c_int, + None, + ); + sqlite3_step(stmt); + sqlite3_finalize(stmt); + stmt = 0 as *mut sqlite3_stmt; + if 0 != set_default { + dc_sqlite3_execute( context, &context.sql, - b"DELETE FROM keypairs WHERE public_key=? OR private_key=?;\x00" as *const u8 - as *const libc::c_char, - ); - let pub_bytes = public_key.to_bytes(); - sqlite3_bind_blob( - stmt, - 1, - pub_bytes.as_ptr() as *const _, - pub_bytes.len() as libc::c_int, - None, - ); - let priv_bytes = private_key.to_bytes(); - sqlite3_bind_blob( - stmt, - 2, - priv_bytes.as_ptr() as *const _, - priv_bytes.len() as libc::c_int, - None, - ); - sqlite3_step(stmt); - sqlite3_finalize(stmt); - stmt = 0 as *mut sqlite3_stmt; - if 0 != set_default { - dc_sqlite3_execute( - context, - &context.sql, - b"UPDATE keypairs SET is_default=0;\x00" as *const u8 as *const libc::c_char, - ); - } - self_addr = dc_sqlite3_get_config( - context, - &context.sql, - b"configured_addr\x00" as *const u8 as *const libc::c_char, - 0 as *const libc::c_char, - ); - if !dc_key_save_self_keypair( - context, - &public_key, - &private_key, - self_addr, - set_default, - &context.sql, - ) { - dc_log_error( - context, - 0i32, - b"Cannot save keypair.\x00" as *const u8 as *const libc::c_char, - ); - } else { - if !buf_preferencrypt.is_null() { - if strcmp( - buf_preferencrypt, - b"nopreference\x00" as *const u8 as *const libc::c_char, - ) == 0i32 - { - dc_sqlite3_set_config_int( - context, - &context.sql, - b"e2ee_enabled\x00" as *const u8 as *const libc::c_char, - 0i32, - ); - } else if strcmp( - buf_preferencrypt, - b"mutual\x00" as *const u8 as *const libc::c_char, - ) == 0i32 - { - dc_sqlite3_set_config_int( - context, - &context.sql, - b"e2ee_enabled\x00" as *const u8 as *const libc::c_char, - 1i32, - ); - } - } - success = 1; - } - } else { - dc_log_error( - context, - 0i32, - b"File does not contain a valid private key.\x00" as *const u8 - as *const libc::c_char, + b"UPDATE keypairs SET is_default=0;\x00" as *const u8 as *const libc::c_char, ); } + self_addr = dc_sqlite3_get_config( + context, + &context.sql, + b"configured_addr\x00" as *const u8 as *const libc::c_char, + 0 as *const libc::c_char, + ); + if !dc_key_save_self_keypair( + context, + &public_key, + &private_key, + self_addr, + set_default, + &context.sql, + ) { + error!(context, 0, "Cannot save keypair.",); + } else { + let prefer_encrypt = header.get("Autocrypt-Prefer-Encrypt"); + + if let Some(prefer_encrypt) = prefer_encrypt { + if prefer_encrypt == "nopreference" { + dc_sqlite3_set_config_int( + context, + &context.sql, + b"e2ee_enabled\x00" as *const u8 as *const libc::c_char, + 0i32, + ); + } else if prefer_encrypt == "mutual" { + dc_sqlite3_set_config_int( + context, + &context.sql, + b"e2ee_enabled\x00" as *const u8 as *const libc::c_char, + 1i32, + ); + } + } + success = 1; + } + } else { + error!(context, 0, "File does not contain a private key.",); } sqlite3_finalize(stmt); - free(buf as *mut libc::c_void); free(self_addr as *mut libc::c_void); success diff --git a/src/key.rs b/src/key.rs index 7744132ab..0b1fc7895 100644 --- a/src/key.rs +++ b/src/key.rs @@ -126,11 +126,31 @@ impl Key { Self::from_binary(data, len, key_type) } + pub fn from_armored_string( + data: &str, + key_type: KeyType, + ) -> Option<(Self, BTreeMap)> { + let bytes = data.as_bytes(); + let res: Result<(Key, _), _> = match key_type { + KeyType::Public => SignedPublicKey::from_armor_single(Cursor::new(bytes)) + .map(|(k, h)| (Into::into(k), h)), + KeyType::Private => SignedSecretKey::from_armor_single(Cursor::new(bytes)) + .map(|(k, h)| (Into::into(k), h)), + }; + + match res { + Ok(res) => Some(res), + Err(err) => { + eprintln!("Invalid key bytes: {:?}", err); + None + } + } + } + pub fn from_base64(encoded_data: &str, key_type: KeyType) -> Option { // strip newlines and other whitespace let cleaned: String = encoded_data.trim().split_whitespace().collect(); let bytes = cleaned.as_bytes(); - base64::decode(bytes) .ok() .and_then(|decoded| Self::from_slice(&decoded, key_type)) @@ -226,16 +246,6 @@ impl Key { .to_string() } - /// the result must be freed - pub fn to_base64_c(&self, break_every: usize) -> *mut libc::c_char { - let res = self.to_base64(break_every); - let res_c = CString::new(res.trim()).unwrap(); - - // need to use strdup to allocate the result with malloc - // so it can be `free`d later. - unsafe { strdup(res_c.as_ptr()) } - } - pub fn to_armored_string( &self, headers: Option<&BTreeMap>, @@ -436,6 +446,73 @@ mod tests { assert_eq!(fingerprint, "1234567890ABCDABCDEFABCDEF"); } + #[test] + fn test_from_armored_string() { + let (private_key, _) = Key::from_armored_string( + "-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +7yPJeQ== +=KZk/ +-----END PGP PRIVATE KEY BLOCK-----", + KeyType::Private, + ) + .expect("failed to decode"); // NOTE: if you take out the ===GU1/ part, everything passes! + let binary = private_key.to_bytes(); + Key::from_slice(&binary, KeyType::Private).expect("invalid private key"); + } + #[test] fn test_format_fingerprint() { let fingerprint = dc_format_fingerprint("1234567890ABCDABCDEFABCDEF1234567890ABCD"); @@ -459,4 +536,21 @@ mod tests { let private_key2 = Key::from_slice(&binary, KeyType::Private).expect("invalid private key"); assert_eq!(private_key, private_key2); } + + #[test] + fn test_ascii_roundtrip() { + let (public_key, private_key) = + crate::pgp::dc_pgp_create_keypair(CString::new("hello").unwrap().as_ptr()).unwrap(); + + let s = public_key.to_armored_string(None).unwrap(); + let (public_key2, _) = + Key::from_armored_string(&s, KeyType::Public).expect("invalid public key"); + assert_eq!(public_key, public_key2); + + let s = private_key.to_armored_string(None).unwrap(); + println!("{}", &s); + let (private_key2, _) = + Key::from_armored_string(&s, KeyType::Private).expect("invalid private key"); + assert_eq!(private_key, private_key2); + } }