Compare commits

...

9 Commits

Author SHA1 Message Date
jikstra
41a87a0626 Use sql migration over direct tables.sql modification 2022-12-01 15:15:04 +01:00
jikstra
d5faf75f56 Fix test_encryption_modus() 2022-12-01 13:50:20 +01:00
jikstra
e3ff786eb7 Add {get, set}_chat_encryption_mods() methdos to jsonrpc 2022-11-29 23:37:30 +01:00
jikstra
540cc8b205 Fix warning of unused result 2022-11-29 23:34:46 +01:00
jikstra
7533f863d1 - Don't pass EncryptionModus by reference
- rename get_encryption_modus() to encryption_modus()
2022-11-18 03:06:40 +01:00
jikstra
bc4eea0c28 cargo fmt 2022-11-18 02:44:19 +01:00
jikstra
d8844a0524 Propagate encryption_mode from Chat to Message 2022-11-18 02:44:02 +01:00
jikstra
a714a4c2da - Add encryption_modus field for msgs table and expose getters/setters on
Message
- Mind the encryption modus in RenderedEmail
2022-11-18 01:26:08 +01:00
jikstra
cb03e93570 Start working on encryption_modus 2022-11-17 22:32:16 +01:00
9 changed files with 239 additions and 8 deletions

View File

@@ -47,7 +47,7 @@ use types::provider_info::ProviderInfo;
use types::webxdc::WebxdcMessageInfo;
use self::types::{
chat::{BasicChat, JSONRPCChatVisibility, MuteDuration},
chat::{BasicChat, JSONRPCChatVisibility, JSONRPCEncryptionModus, MuteDuration},
location::JsonrpcLocation,
message::{
JSONRPCMessageListItem, MessageNotificationInfo, MessageSearchResult, MessageViewtype,
@@ -528,6 +528,8 @@ impl CommandApi {
ChatId::new(chat_id).get_encryption_info(&ctx).await
}
/// Get QR code (text and SVG) that will offer an Setup-Contact or Verified-Group invitation.
/// The QR code is compatible to the OPENPGP4FPR format
/// so that a basic fingerprint comparison also works e.g. with OpenKeychain.
@@ -557,6 +559,32 @@ impl CommandApi {
))
}
async fn set_chat_encryption_modus(
&self,
account_id: u32,
chat_id: u32,
encryption_modus: JSONRPCEncryptionModus
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let chat = ChatId::new(chat_id);
Ok(chat.set_encryption_modus(&ctx, encryption_modus.into_core_type()).await?)
}
async fn get_chat_encryption_modus(
&self,
account_id: u32,
chat_id: u32
) -> Result<Option<JSONRPCEncryptionModus>> {
let ctx = self.get_context(account_id).await?;
let chat = ChatId::new(chat_id);
Ok(
match chat.encryption_modus(&ctx).await? {
Some(encryption_modus) => Some(JSONRPCEncryptionModus::from_core_type(encryption_modus)),
None => None,
}
)
}
/// Continue a Setup-Contact or Verified-Group-Invite protocol
/// started on another device with `get_chat_securejoin_qr_code_svg()`.
/// This function is typically called when `check_qr()` returns

View File

@@ -2,7 +2,7 @@ use std::time::{Duration, SystemTime};
use anyhow::{anyhow, bail, Result};
use deltachat::chat::{self, get_chat_contacts, ChatVisibility};
use deltachat::chat::{Chat, ChatId};
use deltachat::chat::{Chat, ChatId, EncryptionModus};
use deltachat::constants::Chattype;
use deltachat::contact::{Contact, ContactId};
use deltachat::context::Context;
@@ -211,3 +211,33 @@ impl JSONRPCChatVisibility {
}
}
}
#[derive(Clone, Serialize, Deserialize, TypeDef)]
#[serde(rename = "EncryptionModus")]
pub enum JSONRPCEncryptionModus {
Opportunistic = 0,
ForcePlaintext = 1,
ForceEncrypted = 2,
ForceVerified = 3,
}
impl JSONRPCEncryptionModus {
pub fn into_core_type(self) -> EncryptionModus {
match self {
JSONRPCEncryptionModus::Opportunistic => EncryptionModus::Opportunistic,
JSONRPCEncryptionModus::ForcePlaintext => EncryptionModus::ForcePlaintext,
JSONRPCEncryptionModus::ForceEncrypted => EncryptionModus::ForceEncrypted,
JSONRPCEncryptionModus::ForceVerified => EncryptionModus::ForceVerified
}
}
pub fn from_core_type(core_encryption_modus: EncryptionModus) -> Self {
match core_encryption_modus {
EncryptionModus::Opportunistic => JSONRPCEncryptionModus::Opportunistic,
EncryptionModus::ForcePlaintext => JSONRPCEncryptionModus::ForcePlaintext,
EncryptionModus::ForceEncrypted => JSONRPCEncryptionModus::ForceEncrypted,
EncryptionModus::ForceVerified => JSONRPCEncryptionModus::ForceVerified
}
}
}

View File

@@ -331,6 +331,16 @@ export class RawClient {
return (this._transport.request('get_chat_securejoin_qr_code_svg', [accountId, chatId] as RPC.Params)) as Promise<[string,string]>;
}
public setChatEncryptionModus(accountId: T.U32, chatId: T.U32, encryptionModus: T.EncryptionModus): Promise<null> {
return (this._transport.request('set_chat_encryption_modus', [accountId, chatId, encryptionModus] as RPC.Params)) as Promise<null>;
}
public getChatEncryptionModus(accountId: T.U32, chatId: T.U32): Promise<(T.EncryptionModus|null)> {
return (this._transport.request('get_chat_encryption_modus', [accountId, chatId] as RPC.Params)) as Promise<(T.EncryptionModus|null)>;
}
/**
* Continue a Setup-Contact or Verified-Group-Invite protocol
* started on another device with `get_chat_securejoin_qr_code_svg()`.
@@ -730,8 +740,8 @@ export class RawClient {
}
public deleteContact(accountId: T.U32, contactId: T.U32): Promise<boolean> {
return (this._transport.request('delete_contact', [accountId, contactId] as RPC.Params)) as Promise<boolean>;
public deleteContact(accountId: T.U32, contactId: T.U32): Promise<null> {
return (this._transport.request('delete_contact', [accountId, contactId] as RPC.Params)) as Promise<null>;
}

View File

@@ -50,6 +50,7 @@ export type BasicChat=
* used when you only need the basic metadata of a chat like type, name, profile picture
*/
{"id":U32;"name":string;"isProtected":boolean;"profileImage":(string|null);"archived":boolean;"chatType":U32;"isUnpromoted":boolean;"isSelfTalk":boolean;"color":string;"isContactRequest":boolean;"isDeviceChat":boolean;"isMuted":boolean;};
export type EncryptionModus=("Opportunistic"|"ForcePlaintext"|"ForceEncrypted"|"ForceVerified");
export type ChatVisibility=("Normal"|"Archived"|"Pinned");
export type MuteDuration=("NotMuted"|"Forever"|{"Until":I64;});
export type MessageListItem=(({"kind":"message";}&{"msg_id":U32;})|({
@@ -195,4 +196,4 @@ export type MessageNotificationInfo={"id":U32;"chatId":U32;"accountId":U32;"imag
export type MessageSearchResult={"id":U32;"authorProfileImage":(string|null);"authorName":string;"authorColor":string;"chatName":(string|null);"message":string;"timestamp":I64;};
export type F64=number;
export type Location={"locationId":U32;"isIndependent":boolean;"latitude":F64;"longitude":F64;"accuracy":F64;"timestamp":I64;"contactId":U32;"msgId":U32;"chatId":U32;"marker":(string|null);};
export type __AllTyps=[string,boolean,Record<string,string>,U32,U32,null,(U32)[],U32,null,(U32|null),(Account)[],null,null,U32,null,U32,null,U32,Account,U32,U64,U32,string,(ProviderInfo|null),U32,boolean,U32,Record<string,string>,U32,string,(string|null),null,U32,Record<string,(string|null)>,null,U32,string,null,U32,string,Qr,U32,string,(string|null),U32,(string)[],Record<string,(string|null)>,Record<U32,string>,null,U32,null,U32,null,U32,string,(string|null),null,U32,string,(string|null),null,U32,(U32)[],U32,U32,Usize,U32,boolean,I64,Usize,U32,string,U32,U32,string,null,U32,(U32|null),(string|null),(U32|null),(ChatListEntry)[],U32,(ChatListEntry)[],Record<U32,ChatListItemFetchResult>,U32,U32,FullChat,U32,U32,BasicChat,U32,U32,null,U32,U32,null,U32,U32,null,U32,U32,string,U32,(U32|null),[string,string],U32,string,U32,U32,U32,null,U32,U32,U32,null,U32,U32,U32,null,U32,U32,(U32)[],U32,string,boolean,U32,U32,U32,U32,U32,string,null,U32,U32,(string|null),null,U32,U32,ChatVisibility,null,U32,U32,U32,null,U32,U32,U32,U32,string,string,U32,U32,U32,null,U32,U32,(U32|null),U32,U32,MuteDuration,null,U32,U32,boolean,U32,(U32)[],null,U32,U32,U32,(U32)[],U32,U32,U32,(MessageListItem)[],U32,U32,Message,U32,U32,(string|null),U32,(U32)[],Record<U32,Message>,U32,U32,MessageNotificationInfo,U32,(U32)[],null,U32,U32,string,U32,U32,null,U32,string,(U32|null),(U32)[],U32,(U32)[],Record<U32,MessageSearchResult>,U32,U32,Contact,U32,string,(string|null),U32,U32,U32,U32,U32,U32,null,U32,U32,null,U32,(Contact)[],U32,U32,(string|null),(U32)[],U32,U32,(string|null),(Contact)[],U32,(U32)[],Record<U32,Contact>,U32,U32,boolean,U32,U32,string,null,U32,U32,string,U32,string,(U32|null),U32,(U32|null),Viewtype,(Viewtype|null),(Viewtype|null),(U32)[],U32,U32,Viewtype,(Viewtype|null),(Viewtype|null),[(U32|null),(U32|null)],U32,string,(string|null),null,U32,string,(string|null),null,null,U32,U32,U32,string,U32,(U32|null),(U32|null),I64,I64,(Location)[],U32,U32,string,string,null,U32,U32,U32,string,U32,U32,WebxdcMessageInfo,U32,(U32)[],U32,null,U32,U32,string,U32,U32,U32,(string)[],U32,U32,U32,null,U32,U32,(Message|null),U32,U32,U32,U32,string,U32,U32,string,null,U32,Record<string,(string)[]>,U32,U32,string,U32,U32,U32,(string|null),(string|null),([F64,F64]|null),(U32|null),[U32,Message],U32,U32,(string|null),(string|null),(U32|null),null];
export type __AllTyps=[string,boolean,Record<string,string>,U32,U32,null,(U32)[],U32,null,(U32|null),(Account)[],null,null,U32,null,U32,null,U32,Account,U32,U64,U32,string,(ProviderInfo|null),U32,boolean,U32,Record<string,string>,U32,string,(string|null),null,U32,Record<string,(string|null)>,null,U32,string,null,U32,string,Qr,U32,string,(string|null),U32,(string)[],Record<string,(string|null)>,Record<U32,string>,null,U32,null,U32,null,U32,string,(string|null),null,U32,string,(string|null),null,U32,(U32)[],U32,U32,Usize,U32,boolean,I64,Usize,U32,string,U32,U32,string,null,U32,(U32|null),(string|null),(U32|null),(ChatListEntry)[],U32,(ChatListEntry)[],Record<U32,ChatListItemFetchResult>,U32,U32,FullChat,U32,U32,BasicChat,U32,U32,null,U32,U32,null,U32,U32,null,U32,U32,string,U32,(U32|null),[string,string],U32,U32,EncryptionModus,null,U32,U32,(EncryptionModus|null),U32,string,U32,U32,U32,null,U32,U32,U32,null,U32,U32,U32,null,U32,U32,(U32)[],U32,string,boolean,U32,U32,U32,U32,U32,string,null,U32,U32,(string|null),null,U32,U32,ChatVisibility,null,U32,U32,U32,null,U32,U32,U32,U32,string,string,U32,U32,U32,null,U32,U32,(U32|null),U32,U32,MuteDuration,null,U32,U32,boolean,U32,(U32)[],null,U32,U32,U32,(U32)[],U32,U32,U32,(MessageListItem)[],U32,U32,Message,U32,U32,(string|null),U32,(U32)[],Record<U32,Message>,U32,U32,MessageNotificationInfo,U32,(U32)[],null,U32,U32,string,U32,U32,null,U32,string,(U32|null),(U32)[],U32,(U32)[],Record<U32,MessageSearchResult>,U32,U32,Contact,U32,string,(string|null),U32,U32,U32,U32,U32,U32,null,U32,U32,null,U32,(Contact)[],U32,U32,(string|null),(U32)[],U32,U32,(string|null),(Contact)[],U32,(U32)[],Record<U32,Contact>,U32,U32,null,U32,U32,string,null,U32,U32,string,U32,string,(U32|null),U32,(U32|null),Viewtype,(Viewtype|null),(Viewtype|null),(U32)[],U32,U32,Viewtype,(Viewtype|null),(Viewtype|null),[(U32|null),(U32|null)],U32,string,(string|null),null,U32,string,(string|null),null,null,U32,U32,U32,string,U32,(U32|null),(U32|null),I64,I64,(Location)[],U32,U32,string,string,null,U32,U32,U32,string,U32,U32,WebxdcMessageInfo,U32,(U32)[],U32,null,U32,U32,string,U32,U32,U32,(string)[],U32,U32,U32,null,U32,U32,(Message|null),U32,U32,U32,U32,string,U32,U32,string,null,U32,Record<string,(string)[]>,U32,U32,string,U32,U32,U32,(string|null),(string|null),([F64,F64]|null),(U32|null),[U32,Message],U32,U32,(string|null),(string|null),(U32|null),null];

View File

@@ -75,6 +75,29 @@ pub enum ProtectionStatus {
Protected = 1,
}
#[derive(
Debug,
Display,
Clone,
Copy,
PartialEq,
Eq,
FromPrimitive,
ToPrimitive,
FromSql,
ToSql,
IntoStaticStr,
Serialize,
Deserialize,
)]
#[repr(u32)]
pub enum EncryptionModus {
Opportunistic = 0,
ForcePlaintext = 1,
ForceEncrypted = 2,
ForceVerified = 3,
}
impl Default for ProtectionStatus {
fn default() -> Self {
ProtectionStatus::Unprotected
@@ -898,6 +921,38 @@ impl ChatId {
Ok(ret.trim().to_string())
}
/// This sets a protection modus for the chat and enforces that messages are only send if they
/// meet the encryption modus (ForcePlaintext, Opportunistic, ForceEncrypted, ForceVerified)
pub async fn set_encryption_modus(
self,
context: &Context,
modus: EncryptionModus,
) -> Result<()> {
context
.sql
.execute(
"UPDATE chats SET encryption_modus=? WHERE id=?;",
paramsv![modus, self],
)
.await?;
Ok(())
}
/// This sets a protection modus for the chat and enforces that messages are only send if they
/// meet the encryption modus (ForcePlaintext, Opportunistic, ForceEncrypted, ForceVerified)
pub async fn encryption_modus(self, context: &Context) -> Result<Option<EncryptionModus>> {
let encryption_modus: Option<EncryptionModus> = context
.sql
.query_get_value(
"SELECT encryption_modus FROM chats WHERE id=?;",
paramsv![self],
)
.await?;
Ok(encryption_modus)
}
/// Bad evil escape hatch.
///
/// Avoid using this, eventually types should be cleaned up enough
@@ -1944,6 +1999,14 @@ pub async fn is_contact_in_chat(
// the caller can get it from msg.chat_id. Forwards would need to
// be fixed for this somehow too.
pub async fn send_msg(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result<MsgId> {
// Propagate same encryption_mode of chat to message in case messages doesn't yet have an
// encryption_mode
if let None = msg.encryption_modus(&context).await? {
if let Some(encryption_mode) = chat_id.encryption_modus(&context).await? {
msg.set_encryption_modus(&context, encryption_mode).await?;
}
}
if chat_id.is_unset() {
let forwards = msg.param.get(Param::PrepForwards);
if let Some(forwards) = forwards {
@@ -5649,4 +5712,27 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_encryption_modus() -> Result<()> {
let t = TestContext::new_alice().await;
let contact_fiona = Contact::create(&t, "", "fiona@example.net").await?;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "Group").await?;
assert_eq!(
chat_id.encryption_modus(&t).await?,
Some(EncryptionModus::Opportunistic)
);
chat_id
.set_encryption_modus(&t, EncryptionModus::ForceEncrypted)
.await?;
assert_eq!(
chat_id.encryption_modus(&t).await?,
Some(EncryptionModus::ForceEncrypted)
);
Ok(())
}
}

View File

@@ -8,6 +8,7 @@ use deltachat_derive::{FromSql, ToSql};
use rusqlite::types::ValueRef;
use serde::{Deserialize, Serialize};
use crate::chat::EncryptionModus;
use crate::chat::{self, Chat, ChatId};
use crate::config::Config;
use crate::constants::{
@@ -302,6 +303,7 @@ impl Message {
" m.hidden AS hidden,",
" m.location_id AS location,",
" c.blocked AS blocked",
" m.encryption_modus as encryption_modus",
" FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id",
" WHERE m.id=?;"
),
@@ -848,10 +850,39 @@ impl Message {
}
/// Force the message to be sent in plain text.
/// Deprecated: use Message::set_encryption_modus(EncryptionModus::ForcePlaintext)
pub fn force_plaintext(&mut self) {
self.param.set_int(Param::ForcePlaintext, 1);
}
pub async fn set_encryption_modus(
&mut self,
context: &Context,
encryption_modus: EncryptionModus,
) -> Result<()> {
context
.sql
.execute(
"UPDATE msgs SET encryption_modus=? WHERE id=?;",
paramsv![encryption_modus, self.id],
)
.await?;
Ok(())
}
pub async fn encryption_modus(&self, context: &Context) -> Result<Option<EncryptionModus>> {
let encryption_modus: Option<EncryptionModus> = context
.sql
.query_get_value(
"SELECT encryption_modus FROM msgs WHERE id=?;",
paramsv![self.id],
)
.await?;
Ok(encryption_modus)
}
pub async fn update_param(&self, context: &Context) -> Result<()> {
context
.sql

View File

@@ -9,6 +9,7 @@ use tokio::fs;
use crate::blob::BlobObject;
use crate::chat::Chat;
use crate::chat::EncryptionModus;
use crate::config::Config;
use crate::constants::{Chattype, DC_FROM_HANDSHAKE};
use crate::contact::Contact;
@@ -324,7 +325,7 @@ impl<'a> MimeFactory<'a> {
}
}
fn should_force_plaintext(&self) -> bool {
fn should_force_plaintext(&self, encryption_modus: &EncryptionModus) -> bool {
match &self.loaded {
Loaded::Message { chat } => {
if chat.is_protected() {
@@ -333,6 +334,8 @@ impl<'a> MimeFactory<'a> {
// encryption may disclose recipients;
// this is probably a worse issue than not opportunistically (!) encrypting
true
} else if encryption_modus == &EncryptionModus::ForcePlaintext {
true
} else {
self.msg
.param
@@ -602,7 +605,11 @@ impl<'a> MimeFactory<'a> {
let min_verified = self.min_verified();
let grpimage = self.grpimage();
let force_plaintext = self.should_force_plaintext();
let encryption_modus = match self.msg.encryption_modus(context).await? {
Some(encryption_modus) => encryption_modus,
None => EncryptionModus::Opportunistic,
};
let force_plaintext = self.should_force_plaintext(&encryption_modus);
let skip_autocrypt = self.should_skip_autocrypt();
let e2ee_guaranteed = self.is_e2ee_guaranteed();
let encrypt_helper = EncryptHelper::new(context).await?;
@@ -644,6 +651,34 @@ impl<'a> MimeFactory<'a> {
encrypt_helper.should_encrypt(context, e2ee_guaranteed, &peerstates)?;
let is_encrypted = should_encrypt && !force_plaintext;
// Ensure we fulfill encryption_modus
match encryption_modus {
EncryptionModus::Opportunistic => {}
EncryptionModus::ForcePlaintext => {
ensure!(
!is_encrypted,
"EncryptionModus is ForcePlaintext but message is encrypted"
);
}
EncryptionModus::ForceEncrypted => {
ensure!(
is_encrypted,
"EncryptionModus is ForceEncrypted but message is unencrypted"
);
}
EncryptionModus::ForceVerified => {
let chat_is_protected = if let Loaded::Message { chat } = &self.loaded {
chat.is_protected()
} else {
false
};
ensure!(
is_encrypted && chat_is_protected,
"EncryptionModus is ForceVerified but chat is not protected"
);
}
};
let message = if parts.is_empty() {
// Single part, render as regular message.
main_part

View File

@@ -51,6 +51,7 @@ pub enum Param {
ErroneousE2ee = b'e',
/// For Messages: force unencrypted message, a value from `ForcePlaintext` enum.
/// Deprecated: Use EncryptionModus::ForcePlaintext
ForcePlaintext = b'u',
/// For Messages: do not include Autocrypt header.

View File

@@ -10,7 +10,7 @@ use crate::provider::get_provider_by_domain;
use crate::sql::Sql;
use crate::tools::EmailAddress;
const DBVERSION: i32 = 68;
const DBVERSION: i32 = 69;
const VERSION_CFG: &str = "dbversion";
const TABLES: &str = include_str!("./tables.sql");
@@ -616,6 +616,15 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid);
)
.await?;
}
if dbversion < 94 {
sql.execute_migration(
r#"
ALTER TABLE chats ADD COLUMN encryption_modus INTEGER DEFAULT 0;
ALTER TABLE msgs ADD COLUMN encryption_modus INTEGER DEFAULT 0;"#,
94,
)
.await?;
}
let new_version = sql
.get_raw_config_int(VERSION_CFG)