mirror of
https://github.com/chatmail/core.git
synced 2026-04-20 15:06:30 +03:00
Close https://github.com/chatmail/core/issues/7396. Before reviewing,
you should read the issue description of
https://github.com/chatmail/core/issues/7396.
I recommend to review with hidden whitespace changes.
TODO:
- [x] Implement the new protocol
- [x] Make Rust tests pass
- [x] Make Python tests pass
- [x] Test it manually on a phone
- [x] Print the sent messages, and check that they look how they should:
[test_secure_join_group_with_mime_printed.txt](https://github.com/user-attachments/files/24800556/test_secure_join_group.txt)
- [x] Fix bug: If Alice has a second device, then Bob's chat won't be
shown yet on that second device. Also, Bob's contact isn't shown in her
contact list. As soon as either party writes something into the chat,
the that shows up and everything is fine. All of this is still a way
better UX than in WhatsApp, where Bob always has to write first 😂
Still, I should fix that.
- This is actually caused by a larger bug: AUTH tokens aren't synced if
there is no corresponding INVITE token.
- Fixed by 6b658a0e0
- [x] Either make a new `auth_tokens` table with a proper UNIQUE bound,
or put a UNIQUE bound on the `tokens` table
- [x] Benchmarking
- [x] TODOs in the code, maybe change naming of the new functions
- [x] Write test for interop with older DC (esp. that the original
securejoin runs if you remove the &v=3 param)
- [x] From a cryptography perspective, is it fine that vc-request is
encrypted with AUTH, rather than a separate secret (like INVITE)?
- [x] Make sure that QR codes without INVITE work, so that we can remove
it eventually
- [x] Self-review, and comment on some of my code changes to explain
what they do
- [x] ~~Maybe use a new table rather than reusing AUTH token.~~ See
https://github.com/chatmail/core/pull/7754#discussion_r2728544725
- [ ] Update documentation; I'll do that in a separate PR. All necessary
information is in the https://github.com/chatmail/core/issues/7396 issue
description
- [ ] Update tests and other code to use the new names (e.g.
`request-pubkey` rather than `request` and `pubkey` rather than
`auth-required`); I'll do that in a follow-up PR
**Backwards compatibility:**
Everything works seamlessly in my tests. If both devices are updated,
then the new protocol is used; otherwise, the old protocol is used. If
there is a not-yet-updated second device, it will correctly observe the
protocol, and mark the chat partner as verified.
Note that I removed the `Auto-Submitted: auto-replied` header from
securejoin messages. We don't need it ourselves, it's a cleartext header
that leaks too much information, and I can't see any reason to have it.
---------
Co-authored-by: iequidoo <117991069+iequidoo@users.noreply.github.com>
122 lines
3.4 KiB
Rust
122 lines
3.4 KiB
Rust
//! # Token module.
|
|
//!
|
|
//! Functions to read/write token from/to the database. A token is any string associated with a key.
|
|
//!
|
|
//! Tokens are used in SecureJoin verification protocols.
|
|
|
|
use anyhow::Result;
|
|
use deltachat_derive::{FromSql, ToSql};
|
|
|
|
use crate::context::Context;
|
|
use crate::tools::{create_id, time};
|
|
|
|
/// Token namespace
|
|
#[derive(
|
|
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql,
|
|
)]
|
|
#[repr(u32)]
|
|
pub enum Namespace {
|
|
#[default]
|
|
Unknown = 0,
|
|
Auth = 110,
|
|
InviteNumber = 100,
|
|
}
|
|
|
|
/// Saves a token to the database.
|
|
pub async fn save(
|
|
context: &Context,
|
|
namespace: Namespace,
|
|
foreign_key: Option<&str>,
|
|
token: &str,
|
|
timestamp: i64,
|
|
) -> Result<()> {
|
|
if token.is_empty() {
|
|
info!(context, "Not saving empty {namespace} token");
|
|
return Ok(());
|
|
}
|
|
context
|
|
.sql
|
|
.execute(
|
|
"INSERT OR IGNORE INTO tokens (namespc, foreign_key, token, timestamp) VALUES (?, ?, ?, ?)",
|
|
(namespace, foreign_key.unwrap_or(""), token, timestamp),
|
|
)
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Looks up most recently created token for a namespace / foreign key combination.
|
|
///
|
|
/// As there may be more than one such valid token,
|
|
/// (eg. when a qr code token is withdrawn, recreated and revived later),
|
|
/// use lookup() for qr-code creation only;
|
|
/// do not use lookup() to check for token validity.
|
|
///
|
|
/// To check if a given token is valid, use exists().
|
|
pub async fn lookup(
|
|
context: &Context,
|
|
namespace: Namespace,
|
|
foreign_key: Option<&str>,
|
|
) -> Result<Option<String>> {
|
|
context
|
|
.sql
|
|
.query_get_value(
|
|
"SELECT token FROM tokens WHERE namespc=? AND foreign_key=? ORDER BY id DESC LIMIT 1",
|
|
(namespace, foreign_key.unwrap_or("")),
|
|
)
|
|
.await
|
|
}
|
|
|
|
/// Looks up all tokens from the given namespace,
|
|
/// so that they can be used for decrypting a symmetrically-encrypted message.
|
|
///
|
|
/// The most-recently saved tokens are returned first.
|
|
/// This improves performance when Bob scans a QR code that was just created.
|
|
pub async fn lookup_all(context: &Context, namespace: Namespace) -> Result<Vec<String>> {
|
|
context
|
|
.sql
|
|
.query_map_vec(
|
|
"SELECT token FROM tokens WHERE namespc=? ORDER BY id DESC",
|
|
(namespace,),
|
|
|row| Ok(row.get(0)?),
|
|
)
|
|
.await
|
|
}
|
|
|
|
pub async fn lookup_or_new(
|
|
context: &Context,
|
|
namespace: Namespace,
|
|
foreign_key: Option<&str>,
|
|
) -> Result<String> {
|
|
if let Some(token) = lookup(context, namespace, foreign_key).await? {
|
|
return Ok(token);
|
|
}
|
|
|
|
let token = create_id();
|
|
let timestamp = time();
|
|
save(context, namespace, foreign_key, &token, timestamp).await?;
|
|
Ok(token)
|
|
}
|
|
|
|
pub async fn exists(context: &Context, namespace: Namespace, token: &str) -> Result<bool> {
|
|
let exists = context
|
|
.sql
|
|
.exists(
|
|
"SELECT COUNT(*) FROM tokens WHERE namespc=? AND token=?;",
|
|
(namespace, token),
|
|
)
|
|
.await?;
|
|
Ok(exists)
|
|
}
|
|
|
|
/// Resets all tokens corresponding to the `foreign_key`.
|
|
///
|
|
/// `foreign_key` is a group ID to reset all group tokens
|
|
/// or empty string to reset all setup contact tokens.
|
|
pub async fn delete(context: &Context, foreign_key: &str) -> Result<()> {
|
|
context
|
|
.sql
|
|
.execute("DELETE FROM tokens WHERE foreign_key=?", (foreign_key,))
|
|
.await?;
|
|
Ok(())
|
|
}
|