Compare commits

..

5 Commits

Author SHA1 Message Date
Alexander Krotov
ed092fe1ce test_encrypt_decrypt_fuzz_alice: load secret key directly from file 2020-02-29 17:46:55 +03:00
Alexander Krotov
56f589db4a test_encrypt_decrypt_fuzz_alice: load alice public key directly 2020-02-29 17:19:51 +03:00
Alexander Krotov
49b39b42bf Add test_encrypt_decrypt_fuzz_alice
Simplified test using pregenerated key without signature.
2020-02-28 21:28:24 +03:00
Alexander Krotov
7e4951e691 Replace test Alice key with ed25519 key 2020-02-28 05:17:48 +03:00
B. Petersen
f50874acf1 create a basic failing test for the ecc encryption bug 2020-02-27 19:56:50 +01:00
12 changed files with 428 additions and 318 deletions

View File

@@ -1,19 +1,5 @@
# 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

522
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.27.0"
version = "1.25.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.5.1", default-features = false }
pgp = { version = "0.4.0", 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"]
default = ["nightly", "ringbuf"]
vendored = ["async-native-tls/vendored", "reqwest/native-tls-vendored", "async-smtp/native-tls-vendored"]
nightly = ["pgp/nightly"]
ringbuf = ["pgp/ringbuf"]

View File

@@ -108,6 +108,7 @@ $ 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.27.0"
version = "1.25.0"
description = "Deltachat FFI"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
@@ -23,6 +23,7 @@ failure = "0.1.6"
serde_json = "1.0"
[features]
default = ["vendored", "nightly"]
default = ["vendored", "nightly", "ringbuf"]
vendored = ["deltachat/vendored"]
nightly = ["deltachat/nightly"]
ringbuf = ["deltachat/ringbuf"]

View File

@@ -421,18 +421,4 @@ 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
AddressBook = 0x80000,
AdressBook = 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,18 +497,9 @@ impl Contact {
for (name, addr) in split_address_book(addr_book.as_ref()).into_iter() {
let name = normalize_name(name);
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
}
}
let (_, modified) = Contact::add_or_lookup(context, name, addr, Origin::AdressBook)?;
if modified != Modifier::None {
modify_cnt += 1
}
}
if modify_cnt > 0 {
@@ -1238,7 +1229,6 @@ 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 "[...]" to the
/// end of the shortened string.
pub(crate) fn dc_truncate(buf: &str, approx_chars: usize) -> Cow<str> {
let ellipse = "[...]";
/// 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 { "[...]" };
let count = buf.chars().count();
if approx_chars > 0 && count > approx_chars + ellipse.len() {
@@ -538,42 +538,54 @@ mod tests {
#[test]
fn test_dc_truncate_1() {
let s = "this is a little test string";
assert_eq!(dc_truncate(s, 16), "this is a [...]");
assert_eq!(dc_truncate(s, 16, false), "this is a [...]");
assert_eq!(dc_truncate(s, 16, true), "this is a ...");
}
#[test]
fn test_dc_truncate_2() {
assert_eq!(dc_truncate("1234", 2), "1234");
assert_eq!(dc_truncate("1234", 2, false), "1234");
assert_eq!(dc_truncate("1234", 2, true), "1234");
}
#[test]
fn test_dc_truncate_3() {
assert_eq!(dc_truncate("1234567", 1), "1[...]");
assert_eq!(dc_truncate("1234567", 1, false), "1[...]");
assert_eq!(dc_truncate("1234567", 1, true), "1...");
}
#[test]
fn test_dc_truncate_4() {
assert_eq!(dc_truncate("123456", 4), "123456");
assert_eq!(dc_truncate("123456", 4, false), "123456");
assert_eq!(dc_truncate("123456", 4, true), "123456");
}
#[test]
fn test_dc_truncate_edge() {
assert_eq!(dc_truncate("", 4), "");
assert_eq!(dc_truncate("", 4, false), "");
assert_eq!(dc_truncate("", 4, true), "");
assert_eq!(dc_truncate("\n hello \n world", 4), "\n [...]");
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("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 1), "𐠈[...]");
assert_eq!(
dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 0),
dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 1, false),
"𐠈[...]"
);
assert_eq!(
dc_truncate("𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ", 0, false),
"𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ"
);
// 9 characters, so no truncation
assert_eq!(dc_truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠", 6), "𑒀ὐ¢🜀\u{1e01b}A a🟠",);
assert_eq!(
dc_truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠", 6, false),
"𑒀ὐ¢🜀\u{1e01b}A a🟠",
);
// 12 characters, truncation
assert_eq!(
dc_truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠bcd", 6),
dc_truncate("𑒀ὐ¢🜀\u{1e01b}A a🟠bcd", 6, false),
"𑒀ὐ¢🜀\u{1e01b}A[...]",
);
}
@@ -689,10 +701,11 @@ mod tests {
#[test]
fn test_dc_truncate(
buf: String,
approx_chars in 0..10000usize
approx_chars in 0..10000usize,
do_unwrap: bool,
) {
let res = dc_truncate(&buf, approx_chars);
let el_len = 5;
let res = dc_truncate(&buf, approx_chars, do_unwrap);
let el_len = if do_unwrap { 3 } else { 5 };
let l = res.chars().count();
if approx_chars > 0 {
assert!(
@@ -706,7 +719,11 @@ mod tests {
if approx_chars > 0 && buf.chars().count() > approx_chars + el_len {
let l = res.len();
assert_eq!(&res[l-5..l], "[...]", "missing ellipsis in {}", &res);
if do_unwrap {
assert_eq!(&res[l-3..l], "...", "missing ellipsis in {}", &res);
} else {
assert_eq!(&res[l-5..l], "[...]", "missing ellipsis in {}", &res);
}
}
}
}

View File

@@ -4,7 +4,6 @@ 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};
@@ -22,10 +21,6 @@ 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;
@@ -443,7 +438,7 @@ impl Message {
pub fn get_text(&self) -> Option<String> {
self.text
.as_ref()
.map(|text| dc_truncate(text, 30000).to_string())
.map(|text| dc_truncate(text, 30000, false).to_string())
}
pub fn get_filename(&self) -> Option<String> {
@@ -814,7 +809,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);
let rawtxt = dc_truncate(rawtxt.trim(), 100_000, false);
let fts = dc_timestamp_to_str(msg.get_timestamp());
ret += &format!("Sent: {}", fts);
@@ -1146,20 +1141,18 @@ pub fn get_summarytext_by_raw(
return prefix;
}
let summary = if let Some(text) = text {
if let Some(text) = text {
if text.as_ref().is_empty() {
prefix
} else if prefix.is_empty() {
dc_truncate(text.as_ref(), approx_characters).to_string()
dc_truncate(text.as_ref(), approx_characters, true).to_string()
} else {
let tmp = format!("{} {}", prefix, text.as_ref());
dc_truncate(&tmp, approx_characters).to_string()
dc_truncate(&tmp, approx_characters, true).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 | KeyGenType::Default => (PgpKeyType::Rsa(2048), PgpKeyType::Rsa(2048)),
KeyGenType::Ed25519 => (PgpKeyType::EdDSA, PgpKeyType::ECDH),
KeyGenType::Rsa2048 => (PgpKeyType::Rsa(2048), PgpKeyType::Rsa(2048)),
KeyGenType::Ed25519 | KeyGenType::Default => (PgpKeyType::EdDSA, PgpKeyType::ECDH),
};
let user_id = format!("<{}>", addr);
@@ -573,4 +573,88 @@ 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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