Compare commits

..

17 Commits

Author SHA1 Message Date
B. Petersen
0f6024e055 bump version to 1.27 2020-03-04 17:04:26 +01:00
B. Petersen
fafc15f80c changelog 2020-03-04 16:55:44 +01:00
bjoern
9a85ea861d Merge pull request #1327 from deltachat/fix/update-rpgp
fix: update to pgp@0.5
2020-03-04 16:49:56 +01:00
dignifiedquire
9541960307 update fixed rpgp 2020-03-04 15:10:19 +01:00
dignifiedquire
95073deb96 fix: update to pgp@0.5 2020-03-04 14:51:58 +01:00
Alexander Krotov
82b4647b95 Update dc_truncate() comment 2020-03-02 23:09:38 +03:00
Alexander Krotov
0c770a8b37 Fix Origin::AddressBook spelling 2020-03-02 22:57:22 +03:00
B. Petersen
0e4031348f remove unwrap-flag from dc_truncate() - it was never really used on-purpose 2020-03-02 14:10:44 +01:00
B. Petersen
5cc26762c2 unwrap lineends in summaries 2020-03-02 14:10:44 +01:00
B. Petersen
b8b4853d1f add failing test for checking linebreak-unwrapping in get_summary 2020-03-02 14:10:44 +01:00
Alexander Krotov
fbabe27fc1 Make add_or_lookup errors non-fatal in add_address_book() 2020-03-02 14:08:20 +01:00
bjoern
4d1554c85b Merge pull request #1323 from deltachat/prep-1.26
prepare 1.26.0
2020-03-01 22:34:37 +01:00
B. Petersen
ab40495d5c update rPGP in .toml, this was missed in #1321 2020-03-01 21:54:11 +01:00
B. Petersen
42ebf49f92 bump version to 1.26.0 2020-03-01 21:46:26 +01:00
B. Petersen
c5ccd88f79 changelog 2020-03-01 21:43:57 +01:00
B. Petersen
dbd1b227d9 revert generating ecc keys for now
the currently released versions fail sometimes in encrypting to ecc keys,
see #1313, the issue is about to be fixed,
however, we should not generate ecc keys until the
fix is rolled out - otherwise new users will get encryption errors every some
messages if their counterpart is not yet using the most recent version.

we can start generating ecc keys a few weeks after the fix is rolled out.
2020-03-01 21:34:04 +03:00
Alexander Krotov
63baac3c61 Update rPGP to 0.4.1
This version fixes incorrect serialization of ECC session keys with
leading zeros.
2020-03-01 20:52:08 +03:00
12 changed files with 317 additions and 427 deletions

View File

@@ -1,5 +1,19 @@
# Changelog
## 1.27.0
- handle keys reliably on armv7 #1327
## 1.26.0
- change generated key type back to RSA as shipped versions
have problems to encrypt to Ed25519 keys
- update rPGP to encrypt reliably to Ed25519 keys;
one of the next versions can finally use Ed25519 keys then
## 1.25.0
- save traffic by downloading only messages that are really displayed #1236

520
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.25.0"
version = "1.27.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
license = "MPL-2.0"
@@ -12,7 +12,7 @@ lto = true
deltachat_derive = { path = "./deltachat_derive" }
libc = "0.2.51"
pgp = { version = "0.4.0", default-features = false }
pgp = { version = "0.5.1", default-features = false }
hex = "0.4.0"
sha2 = "0.8.0"
rand = "0.7.0"
@@ -84,7 +84,7 @@ required-features = ["rustyline"]
[features]
default = ["nightly", "ringbuf"]
default = ["nightly"]
vendored = ["async-native-tls/vendored", "reqwest/native-tls-vendored", "async-smtp/native-tls-vendored"]
nightly = ["pgp/nightly"]
ringbuf = ["pgp/ringbuf"]

View File

@@ -108,7 +108,6 @@ $ cargo test -- --ignored
- `vendored`: When using Openssl for TLS, this bundles a vendored version.
- `nightly`: Enable nightly only performance and security related features.
- `ringbuf`: Enable the use of [`slice_deque`](https://github.com/gnzlbg/slice_deque) in pgp.
[circle-shield]: https://img.shields.io/circleci/project/github/deltachat/deltachat-core-rust/master.svg?style=flat-square
[circle]: https://circleci.com/gh/deltachat/deltachat-core-rust/

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.25.0"
version = "1.27.0"
description = "Deltachat FFI"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
@@ -23,7 +23,6 @@ failure = "0.1.6"
serde_json = "1.0"
[features]
default = ["vendored", "nightly", "ringbuf"]
default = ["vendored", "nightly"]
vendored = ["deltachat/vendored"]
nightly = ["deltachat/nightly"]
ringbuf = ["deltachat/ringbuf"]

View File

@@ -421,4 +421,18 @@ mod tests {
let chats = Chatlist::try_load(&t.ctx, 0, Some("t-5678-b"), None).unwrap();
assert_eq!(chats.len(), 1);
}
#[test]
fn test_get_summary_unwrap() {
let t = dummy_context();
let chat_id1 = create_group_chat(&t.ctx, VerifiedStatus::Unverified, "a chat").unwrap();
let mut msg = Message::new(Viewtype::Text);
msg.set_text(Some("foo:\nbar \r\n test".to_string()));
chat_id1.set_draft(&t.ctx, Some(&mut msg));
let chats = Chatlist::try_load(&t.ctx, 0, None, None).unwrap();
let summary = chats.get_summary(&t.ctx, 0, None);
assert_eq!(summary.get_text2().unwrap(), "foo: bar test"); // the linebreak should be removed from summary
}
}

View File

@@ -118,7 +118,7 @@ pub enum Origin {
Internal = 0x40000,
/// address is in our address book
AdressBook = 0x80000,
AddressBook = 0x80000,
/// set on Alice's side for contacts like Bob that have scanned the QR code offered by her. Only means the contact has once been established using the "securejoin" procedure in the past, getting the current key verification status requires calling dc_contact_is_verified() !
SecurejoinInvited = 0x0100_0000,
@@ -497,9 +497,18 @@ impl Contact {
for (name, addr) in split_address_book(addr_book.as_ref()).into_iter() {
let name = normalize_name(name);
let (_, modified) = Contact::add_or_lookup(context, name, addr, Origin::AdressBook)?;
if modified != Modifier::None {
modify_cnt += 1
match Contact::add_or_lookup(context, name, addr, Origin::AddressBook) {
Err(err) => {
warn!(
context,
"Failed to add address {} from address book: {}", addr, err
);
}
Ok((_, modified)) => {
if modified != Modifier::None {
modify_cnt += 1
}
}
}
}
if modify_cnt > 0 {
@@ -1229,6 +1238,7 @@ mod tests {
let book = concat!(
" Name one \n one@eins.org \n",
"Name two\ntwo@deux.net\n",
"Invalid\n+1234567890\n", // invalid, should be ignored
"\nthree@drei.sam\n",
"Name two\ntwo@deux.net\n" // should not be added again
);

View File

@@ -19,10 +19,10 @@ pub(crate) fn dc_exactly_one_bit_set(v: i32) -> bool {
0 != v && 0 == v & (v - 1)
}
/// Shortens a string to a specified length and adds "..." or "[...]" to the end of
/// the shortened string.
pub(crate) fn dc_truncate(buf: &str, approx_chars: usize, do_unwrap: bool) -> Cow<str> {
let ellipse = if do_unwrap { "..." } else { "[...]" };
/// Shortens a string to a specified length and adds "[...]" to the
/// end of the shortened string.
pub(crate) fn dc_truncate(buf: &str, approx_chars: usize) -> Cow<str> {
let ellipse = "[...]";
let count = buf.chars().count();
if approx_chars > 0 && count > approx_chars + ellipse.len() {
@@ -538,54 +538,42 @@ mod tests {
#[test]
fn test_dc_truncate_1() {
let s = "this is a little test string";
assert_eq!(dc_truncate(s, 16, false), "this is a [...]");
assert_eq!(dc_truncate(s, 16, true), "this is a ...");
assert_eq!(dc_truncate(s, 16), "this is a [...]");
}
#[test]
fn test_dc_truncate_2() {
assert_eq!(dc_truncate("1234", 2, false), "1234");
assert_eq!(dc_truncate("1234", 2, true), "1234");
assert_eq!(dc_truncate("1234", 2), "1234");
}
#[test]
fn test_dc_truncate_3() {
assert_eq!(dc_truncate("1234567", 1, false), "1[...]");
assert_eq!(dc_truncate("1234567", 1, true), "1...");
assert_eq!(dc_truncate("1234567", 1), "1[...]");
}
#[test]
fn test_dc_truncate_4() {
assert_eq!(dc_truncate("123456", 4, false), "123456");
assert_eq!(dc_truncate("123456", 4, true), "123456");
assert_eq!(dc_truncate("123456", 4), "123456");
}
#[test]
fn test_dc_truncate_edge() {
assert_eq!(dc_truncate("", 4, false), "");
assert_eq!(dc_truncate("", 4, true), "");
assert_eq!(dc_truncate("", 4), "");
assert_eq!(dc_truncate("\n hello \n world", 4, false), "\n [...]");
assert_eq!(dc_truncate("\n hello \n world", 4, true), "\n ...");
assert_eq!(dc_truncate("\n hello \n world", 4), "\n [...]");
assert_eq!(dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 1), "𐠈[...]");
assert_eq!(
dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 1, false),
"𐠈[...]"
);
assert_eq!(
dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 0, false),
dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 0),
"𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ"
);
// 9 characters, so no truncation
assert_eq!(
dc_truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠", 6, false),
"𑒀ὐ¢🜀\u{1e01b}A a🟠",
);
assert_eq!(dc_truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠", 6), "𑒀ὐ¢🜀\u{1e01b}A a🟠",);
// 12 characters, truncation
assert_eq!(
dc_truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠bcd", 6, false),
dc_truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠bcd", 6),
"𑒀ὐ¢🜀\u{1e01b}A[...]",
);
}
@@ -701,11 +689,10 @@ mod tests {
#[test]
fn test_dc_truncate(
buf: String,
approx_chars in 0..10000usize,
do_unwrap: bool,
approx_chars in 0..10000usize
) {
let res = dc_truncate(&buf, approx_chars, do_unwrap);
let el_len = if do_unwrap { 3 } else { 5 };
let res = dc_truncate(&buf, approx_chars);
let el_len = 5;
let l = res.chars().count();
if approx_chars > 0 {
assert!(
@@ -719,11 +706,7 @@ mod tests {
if approx_chars > 0 && buf.chars().count() > approx_chars + el_len {
let l = res.len();
if do_unwrap {
assert_eq!(&res[l-3..l], "...", "missing ellipsis in {}", &res);
} else {
assert_eq!(&res[l-5..l], "[...]", "missing ellipsis in {}", &res);
}
assert_eq!(&res[l-5..l], "[...]", "missing ellipsis in {}", &res);
}
}
}

View File

@@ -4,6 +4,7 @@ use std::path::{Path, PathBuf};
use deltachat_derive::{FromSql, ToSql};
use failure::Fail;
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use crate::chat::{self, Chat, ChatId};
@@ -21,6 +22,10 @@ use crate::pgp::*;
use crate::sql;
use crate::stock::StockMessage;
lazy_static! {
static ref UNWRAP_RE: regex::Regex = regex::Regex::new(r"\s+").unwrap();
}
// In practice, the user additionally cuts the string themselves
// pixel-accurate.
const SUMMARY_CHARACTERS: usize = 160;
@@ -438,7 +443,7 @@ impl Message {
pub fn get_text(&self) -> Option<String> {
self.text
.as_ref()
.map(|text| dc_truncate(text, 30000, false).to_string())
.map(|text| dc_truncate(text, 30000).to_string())
}
pub fn get_filename(&self) -> Option<String> {
@@ -809,7 +814,7 @@ pub fn get_msg_info(context: &Context, msg_id: MsgId) -> String {
return ret;
}
let rawtxt = rawtxt.unwrap_or_default();
let rawtxt = dc_truncate(rawtxt.trim(), 100_000, false);
let rawtxt = dc_truncate(rawtxt.trim(), 100_000);
let fts = dc_timestamp_to_str(msg.get_timestamp());
ret += &format!("Sent: {}", fts);
@@ -1141,18 +1146,20 @@ pub fn get_summarytext_by_raw(
return prefix;
}
if let Some(text) = text {
let summary = if let Some(text) = text {
if text.as_ref().is_empty() {
prefix
} else if prefix.is_empty() {
dc_truncate(text.as_ref(), approx_characters, true).to_string()
dc_truncate(text.as_ref(), approx_characters).to_string()
} else {
let tmp = format!("{} {}", prefix, text.as_ref());
dc_truncate(&tmp, approx_characters, true).to_string()
dc_truncate(&tmp, approx_characters).to_string()
}
} else {
prefix
}
};
UNWRAP_RE.replace_all(&summary, " ").to_string()
}
// as we do not cut inside words, this results in about 32-42 characters.

View File

@@ -153,8 +153,8 @@ pub(crate) fn create_keypair(
keygen_type: KeyGenType,
) -> std::result::Result<KeyPair, PgpKeygenError> {
let (secret_key_type, public_key_type) = match keygen_type {
KeyGenType::Rsa2048 => (PgpKeyType::Rsa(2048), PgpKeyType::Rsa(2048)),
KeyGenType::Ed25519 | KeyGenType::Default => (PgpKeyType::EdDSA, PgpKeyType::ECDH),
KeyGenType::Rsa2048 | KeyGenType::Default => (PgpKeyType::Rsa(2048), PgpKeyType::Rsa(2048)),
KeyGenType::Ed25519 => (PgpKeyType::EdDSA, PgpKeyType::ECDH),
};
let user_id = format!("<{}>", addr);
@@ -573,88 +573,4 @@ mod tests {
.unwrap();
assert_eq!(plain, CLEARTEXT);
}
fn test_encrypt_decrypt_fuzz(key_type: KeyGenType) {
// sending messages from Alice to Bob
let alice = create_keypair(EmailAddress::new("a@e.org").unwrap(), key_type).unwrap();
let bob = create_keypair(EmailAddress::new("b@e.org").unwrap(), key_type).unwrap();
let alice_secret = Key::from(alice.secret.clone());
let alice_public = Key::from(alice.public.clone());
let bob_secret = Key::from(bob.secret.clone());
let bob_public = Key::from(bob.public.clone());
let plain: &[u8] = b"just a test";
let mut encr_keyring = Keyring::default();
encr_keyring.add_ref(&bob_public);
let mut decr_keyring = Keyring::default();
decr_keyring.add_ref(&bob_secret);
let mut validate_keyring = Keyring::default();
validate_keyring.add_ref(&alice_public);
for _ in 0..1000 {
let ctext = pk_encrypt(plain.as_ref(), &encr_keyring, Some(&alice_secret)).unwrap();
let plain2 =
pk_decrypt(ctext.as_ref(), &decr_keyring, &validate_keyring, None).unwrap();
assert_eq!(plain2, plain);
}
}
#[test]
#[ignore]
fn test_encrypt_decrypt_fuzz_rsa() {
test_encrypt_decrypt_fuzz(KeyGenType::Rsa2048)
}
#[test]
fn test_encrypt_decrypt_fuzz_ecc() {
test_encrypt_decrypt_fuzz(KeyGenType::Ed25519)
}
#[test]
fn test_encrypt_decrypt_fuzz_alice() -> Result<()> {
let plain: &[u8] = b"just a test";
let lit_msg = Message::new_literal_bytes("", plain);
let mut rng = thread_rng();
let enc_key =
SignedPublicKey::from_base64(include_str!("../test-data/key/alice-public.asc"))
.unwrap();
let enc_subkey = &enc_key.public_subkeys[0];
assert!(enc_subkey.is_encryption_key());
let pkeys: Vec<&SignedPublicSubKey> = vec![&enc_subkey];
let skey = SignedSecretKey::from_base64(include_str!("../test-data/key/alice-secret.asc"))
.unwrap();
let skeys: Vec<&SignedSecretKey> = vec![&skey];
for _ in 0..1000 {
let msg = lit_msg.encrypt_to_keys(
&mut rng,
pgp::crypto::sym::SymmetricKeyAlgorithm::AES128,
&pkeys[..],
)?;
let ctext = msg.to_armored_string(None)?;
println!("{}", ctext);
let (decryptor, _) = msg.decrypt(|| "".into(), || "".into(), &skeys[..])?;
let msgs = decryptor.collect::<pgp::errors::Result<Vec<_>>>()?;
ensure!(!msgs.is_empty(), "No valid messages found");
let dec_msg = &msgs[0];
let plain2: Vec<u8> = match dec_msg.get_content()? {
Some(content) => content,
None => bail!("Decrypted message is empty"),
};
assert_eq!(plain2, plain);
}
Ok(())
}
}

View File

@@ -1 +1 @@
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

View File

@@ -1 +1 @@
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