mirror of
https://forgejo.ellis.link/continuwuation/continuwuity/
synced 2026-07-06 04:31:37 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f824792b9 | |||
| 9ba406761b | |||
| 97f49d6357 | |||
| 1a49bc6f87 | |||
| 833216256b | |||
| 5fa3087401 |
@@ -45,7 +45,6 @@
|
||||
- [ ] I have [tested my contribution][c1t] (or proof-read it for documentation-only changes)
|
||||
myself, if applicable. This includes ensuring code compiles.
|
||||
- [ ] My commit messages follow the [commit message format][c1cm] and are descriptive.
|
||||
- [ ] I have written a [news fragment][n1] for this PR, if applicable<!--(can be done after hitting open!)-->.
|
||||
|
||||
<!--
|
||||
Notes on these requirements:
|
||||
@@ -79,4 +78,3 @@
|
||||
[c1pc]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/CONTRIBUTING.md#pre-commit-checks
|
||||
[c1t]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/CONTRIBUTING.md#running-tests-locally
|
||||
[c1cm]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/CONTRIBUTING.md#commit-messages
|
||||
[n1]: https://towncrier.readthedocs.io/en/stable/tutorial.html#creating-news-fragments
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Added support for fallback encryption keys.
|
||||
@@ -1934,102 +1934,6 @@
|
||||
#
|
||||
#foci = []
|
||||
|
||||
[global.ldap]
|
||||
|
||||
# Whether to enable LDAP login.
|
||||
#
|
||||
# example: "true"
|
||||
#
|
||||
#enable = false
|
||||
|
||||
# Whether to force LDAP authentication or authorize classical password
|
||||
# login.
|
||||
#
|
||||
# example: "true"
|
||||
#
|
||||
#ldap_only = false
|
||||
|
||||
# URI of the LDAP server.
|
||||
#
|
||||
# example: "ldap://ldap.example.com:389"
|
||||
#
|
||||
#uri = ""
|
||||
|
||||
# StartTLS for LDAP connections.
|
||||
#
|
||||
#use_starttls = false
|
||||
|
||||
# Skip TLS certificate verification, possibly dangerous.
|
||||
#
|
||||
#disable_tls_verification = false
|
||||
|
||||
# Root of the searches.
|
||||
#
|
||||
# example: "ou=users,dc=example,dc=org"
|
||||
#
|
||||
#base_dn = ""
|
||||
|
||||
# Bind DN if anonymous search is not enabled.
|
||||
#
|
||||
# You can use the variable `{username}` that will be replaced by the
|
||||
# entered username. In such case, the password used to bind will be the
|
||||
# one provided for the login and not the one given by
|
||||
# `bind_password_file`. Beware: automatically granting admin rights will
|
||||
# not work if you use this direct bind instead of a LDAP search.
|
||||
#
|
||||
# example: "cn=ldap-reader,dc=example,dc=org" or
|
||||
# "cn={username},ou=users,dc=example,dc=org"
|
||||
#
|
||||
#bind_dn = ""
|
||||
|
||||
# Path to a file on the system that contains the password for the
|
||||
# `bind_dn`.
|
||||
#
|
||||
# The server must be able to access the file, and it must not be empty.
|
||||
#
|
||||
#bind_password_file = ""
|
||||
|
||||
# Search filter to limit user searches.
|
||||
#
|
||||
# You can use the variable `{username}` that will be replaced by the
|
||||
# entered username for more complex filters.
|
||||
#
|
||||
# example: "(&(objectClass=person)(memberOf=matrix))"
|
||||
#
|
||||
#filter = "(objectClass=*)"
|
||||
|
||||
# Attribute to use to uniquely identify the user.
|
||||
#
|
||||
# example: "uid" or "cn"
|
||||
#
|
||||
#uid_attribute = "uid"
|
||||
|
||||
# Attribute containing the display name of the user.
|
||||
#
|
||||
# example: "givenName" or "sn"
|
||||
#
|
||||
#name_attribute = "givenName"
|
||||
|
||||
# Root of the searches for admin users.
|
||||
#
|
||||
# Defaults to `base_dn` if empty.
|
||||
#
|
||||
# example: "ou=admins,dc=example,dc=org"
|
||||
#
|
||||
#admin_base_dn = ""
|
||||
|
||||
# The LDAP search filter to find administrative users for continuwuity.
|
||||
#
|
||||
# If left blank, administrative state must be configured manually for each
|
||||
# user.
|
||||
#
|
||||
# You can use the variable `{username}` that will be replaced by the
|
||||
# entered username for more complex filters.
|
||||
#
|
||||
# example: "(objectClass=conduwuitAdmin)" or "(uid={username})"
|
||||
#
|
||||
#admin_filter = ""
|
||||
|
||||
#[global.antispam]
|
||||
|
||||
#[global.antispam.meowlnir]
|
||||
|
||||
@@ -64,6 +64,27 @@ pub(crate) async fn upload_keys_route(
|
||||
.await?;
|
||||
}
|
||||
|
||||
for (key_id, fallback_key) in &body.fallback_keys {
|
||||
if fallback_key
|
||||
.deserialize()
|
||||
.inspect_err(|e| {
|
||||
debug_warn!(
|
||||
%key_id,
|
||||
?fallback_key,
|
||||
"Invalid one time key JSON submitted by client, skipping: {e}"
|
||||
);
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
services
|
||||
.users
|
||||
.add_fallback_key(sender_user, sender_device, key_id, fallback_key, false)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(device_keys) = &body.device_keys {
|
||||
let deser_device_keys = device_keys.deserialize().map_err(|e| {
|
||||
err!(Request(BadJson(debug_warn!(
|
||||
|
||||
@@ -395,6 +395,10 @@ pub(crate) async fn build_sync_events(
|
||||
.users
|
||||
.count_one_time_keys(syncing_user, syncing_device);
|
||||
|
||||
let unused_fallback_key_types = services
|
||||
.users
|
||||
.list_unused_fallback_key_types(syncing_user, syncing_device);
|
||||
|
||||
let (
|
||||
(joined_rooms, mut device_list_updates),
|
||||
left_rooms,
|
||||
@@ -405,6 +409,7 @@ pub(crate) async fn build_sync_events(
|
||||
to_device_events,
|
||||
keys_changed,
|
||||
device_one_time_keys_count,
|
||||
unused_fallback_key_types,
|
||||
) = async {
|
||||
futures::join!(
|
||||
joined_rooms,
|
||||
@@ -415,7 +420,8 @@ pub(crate) async fn build_sync_events(
|
||||
account_data,
|
||||
to_device_events,
|
||||
keys_changed,
|
||||
device_one_time_keys_count
|
||||
device_one_time_keys_count,
|
||||
unused_fallback_key_types,
|
||||
)
|
||||
}
|
||||
.boxed()
|
||||
@@ -433,8 +439,7 @@ pub(crate) async fn build_sync_events(
|
||||
account_data: assign!(GlobalAccountData::new(), { events: account_data }),
|
||||
device_lists: device_list_updates.into(),
|
||||
device_one_time_keys_count,
|
||||
// Fallback keys are not yet supported
|
||||
device_unused_fallback_key_types: None,
|
||||
device_unused_fallback_key_types: Some(unused_fallback_key_types),
|
||||
presence: assign!(Presence::new(), {
|
||||
events: presence_updates
|
||||
.into_iter()
|
||||
|
||||
+8
-2
@@ -288,8 +288,14 @@ fn deserialize_option<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
|
||||
}
|
||||
|
||||
#[cfg_attr(unabridged, tracing::instrument(level = "trace", skip_all))]
|
||||
fn deserialize_bool<V: Visitor<'de>>(self, _visitor: V) -> Result<V::Value> {
|
||||
unhandled!("deserialize bool not implemented")
|
||||
fn deserialize_bool<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
|
||||
let byte = self
|
||||
.buf
|
||||
.get(self.pos)
|
||||
.ok_or(Self::Error::SerdeDe("bool buffer underflow".into()))?;
|
||||
self.inc_pos(1);
|
||||
|
||||
visitor.visit_bool(*byte != 0x00)
|
||||
}
|
||||
|
||||
#[cfg_attr(unabridged, tracing::instrument(level = "trace", skip_all))]
|
||||
|
||||
@@ -120,6 +120,10 @@ pub(super) fn open_list(db: &Arc<Engine>, maps: &[Descriptor]) -> Result<Maps> {
|
||||
name: "onetimekeyid_onetimekeys",
|
||||
..descriptor::RANDOM_SMALL
|
||||
},
|
||||
Descriptor {
|
||||
name: "fallbackkeyid_fallbackkey",
|
||||
..descriptor::RANDOM_SMALL
|
||||
},
|
||||
Descriptor {
|
||||
name: "passwordresettoken_info",
|
||||
..descriptor::RANDOM_SMALL
|
||||
|
||||
+2
-2
@@ -297,8 +297,8 @@ fn serialize_u16(self, _v: u16) -> Result<Self::Ok> {
|
||||
|
||||
fn serialize_u8(self, v: u8) -> Result<Self::Ok> { self.write(&[v]) }
|
||||
|
||||
fn serialize_bool(self, _v: bool) -> Result<Self::Ok> {
|
||||
unhandled!("serialize bool not implemented")
|
||||
fn serialize_bool(self, v: bool) -> Result<Self::Ok> {
|
||||
if v { self.write(&[0x01]) } else { self.write(&[0x00]) }
|
||||
}
|
||||
|
||||
fn serialize_unit(self) -> Result<Self::Ok> { unhandled!("serialize unit not implemented") }
|
||||
|
||||
+15
-14
@@ -42,26 +42,27 @@ assets = [
|
||||
[features]
|
||||
default = [
|
||||
"standard",
|
||||
"release_max_log_level",
|
||||
#"release_max_log_level",
|
||||
"ring",
|
||||
"bindgen-runtime", # replace with bindgen-static on alpine
|
||||
"bindgen-runtime", # replace with bindgen-static on alpine
|
||||
]
|
||||
standard = [
|
||||
"blurhashing",
|
||||
"brotli_compression",
|
||||
"element_hacks",
|
||||
"gzip_compression",
|
||||
"io_uring",
|
||||
"jemalloc",
|
||||
"jemalloc_conf",
|
||||
"journald",
|
||||
"media_thumbnail",
|
||||
"systemd",
|
||||
"url_preview",
|
||||
"zstd_compression",
|
||||
"brotli_compression",
|
||||
"element_hacks",
|
||||
"gzip_compression",
|
||||
"io_uring",
|
||||
"jemalloc",
|
||||
"jemalloc_conf",
|
||||
"journald",
|
||||
"media_thumbnail",
|
||||
"systemd",
|
||||
"url_preview",
|
||||
"zstd_compression",
|
||||
"sentry_telemetry",
|
||||
"otlp_telemetry",
|
||||
"console",
|
||||
"console",
|
||||
"direct_tls",
|
||||
]
|
||||
full = [
|
||||
"standard",
|
||||
|
||||
+105
-7
@@ -9,8 +9,8 @@
|
||||
use database::{Deserialized, Ignore, Interfix, Json, Map};
|
||||
use futures::{Stream, StreamExt, TryFutureExt};
|
||||
use ruma::{
|
||||
DeviceId, KeyId, MilliSecondsSinceUnixEpoch, OneTimeKeyAlgorithm, OneTimeKeyId,
|
||||
OneTimeKeyName, OwnedDeviceId, OwnedKeyId, OwnedMxcUri, OwnedUserId, RoomId, UInt, UserId,
|
||||
DeviceId, MilliSecondsSinceUnixEpoch, OneTimeKeyAlgorithm, OneTimeKeyId, OneTimeKeyName,
|
||||
OwnedDeviceId, OwnedKeyId, OwnedMxcUri, OwnedOneTimeKeyId, OwnedUserId, RoomId, UInt, UserId,
|
||||
api::{
|
||||
client::{device::Device, filter::FilterDefinition},
|
||||
error::ErrorKind,
|
||||
@@ -57,6 +57,7 @@ struct Data {
|
||||
keychangeid_userid: Arc<Map>,
|
||||
keyid_key: Arc<Map>,
|
||||
onetimekeyid_onetimekeys: Arc<Map>,
|
||||
fallbackkeyid_fallbackkey: Arc<Map>,
|
||||
openidtoken_expiresatuserid: Arc<Map>,
|
||||
logintoken_expiresatuserid: Arc<Map>,
|
||||
todeviceid_events: Arc<Map>,
|
||||
@@ -97,6 +98,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||
keychangeid_userid: args.db["keychangeid_userid"].clone(),
|
||||
keyid_key: args.db["keyid_key"].clone(),
|
||||
onetimekeyid_onetimekeys: args.db["onetimekeyid_onetimekeys"].clone(),
|
||||
fallbackkeyid_fallbackkey: args.db["fallbackkeyid_fallbackkey"].clone(),
|
||||
openidtoken_expiresatuserid: args.db["openidtoken_expiresatuserid"].clone(),
|
||||
logintoken_expiresatuserid: args.db["logintoken_expiresatuserid"].clone(),
|
||||
todeviceid_events: args.db["todeviceid_events"].clone(),
|
||||
@@ -550,7 +552,7 @@ pub async fn add_one_time_key(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
device_id: &DeviceId,
|
||||
one_time_key_key: &KeyId<OneTimeKeyAlgorithm, OneTimeKeyName>,
|
||||
one_time_key_key: &OneTimeKeyId,
|
||||
one_time_key_value: &Raw<OneTimeKey>,
|
||||
) -> Result {
|
||||
// All devices have metadata
|
||||
@@ -587,6 +589,39 @@ pub async fn add_one_time_key(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Save a fallback key for the given user, device, and algorithm
|
||||
/// This key will replace an existing fallback key
|
||||
pub async fn add_fallback_key(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
device_id: &DeviceId,
|
||||
fallback_key_id: &OneTimeKeyId,
|
||||
fallback_key: &Raw<OneTimeKey>,
|
||||
used: bool,
|
||||
) -> Result {
|
||||
// All devices have metadata
|
||||
// Only existing devices should be able to call this, but we shouldn't assert
|
||||
// either...
|
||||
let key = (user_id, device_id);
|
||||
if self.db.userdeviceid_metadata.qry(&key).await.is_err() {
|
||||
return Err!(Database(error!(
|
||||
%user_id,
|
||||
%device_id,
|
||||
"User does not exist or device has no metadata."
|
||||
)));
|
||||
}
|
||||
|
||||
// There is one fallback key slot per user, per device, per algorithm
|
||||
// Therefore we use this as the DB key for this column
|
||||
let db_key = (user_id, device_id, fallback_key_id.algorithm());
|
||||
|
||||
self.db
|
||||
.fallbackkeyid_fallbackkey
|
||||
.put(db_key, (used, fallback_key_id.as_str(), Json(fallback_key)));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn last_one_time_keys_update(&self, user_id: &UserId) -> u64 {
|
||||
self.db
|
||||
.userid_lastonetimekeyupdate
|
||||
@@ -618,6 +653,8 @@ pub async fn take_one_time_key(
|
||||
.onetimekeyid_onetimekeys
|
||||
.raw_stream_prefix(&prefix)
|
||||
.ignore_err()
|
||||
.next()
|
||||
.await
|
||||
.map(|(key, val)| {
|
||||
self.db.onetimekeyid_onetimekeys.remove(key);
|
||||
|
||||
@@ -636,11 +673,44 @@ pub async fn take_one_time_key(
|
||||
.unwrap();
|
||||
|
||||
(key, val)
|
||||
})
|
||||
.next()
|
||||
.await;
|
||||
});
|
||||
|
||||
one_time_key.ok_or_else(|| err!(Request(NotFound("No one-time-key found"))))
|
||||
if let Some(result) = one_time_key {
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
// No one-time key has been found. Look for a fallback key.
|
||||
|
||||
let db_key = (user_id, device_id, key_algorithm);
|
||||
|
||||
let fallback_key = self
|
||||
.db
|
||||
.fallbackkeyid_fallbackkey
|
||||
.qry(&db_key)
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|handle| {
|
||||
handle
|
||||
.deserialized::<(bool, OwnedOneTimeKeyId, Raw<OneTimeKey>)>()
|
||||
.ok()
|
||||
});
|
||||
|
||||
if let Some((used, fallback_key_id, fallback_key_value)) = fallback_key {
|
||||
if !used {
|
||||
// write the key to the database again to mark it as used
|
||||
self.add_fallback_key(
|
||||
user_id,
|
||||
device_id,
|
||||
&fallback_key_id,
|
||||
&fallback_key_value,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
return Ok((fallback_key_id, fallback_key_value));
|
||||
}
|
||||
|
||||
Err(err!(Request(NotFound("No one-time key or fallback key found"))))
|
||||
}
|
||||
|
||||
pub async fn count_one_time_keys(
|
||||
@@ -673,6 +743,34 @@ pub async fn count_one_time_keys(
|
||||
algorithm_counts
|
||||
}
|
||||
|
||||
pub async fn list_unused_fallback_key_types(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
device_id: &DeviceId,
|
||||
) -> Vec<OneTimeKeyAlgorithm> {
|
||||
type KeyVal = ((String, String, OneTimeKeyAlgorithm), (bool, String, Ignore));
|
||||
|
||||
let mut query = user_id.as_bytes().to_vec();
|
||||
query.push(0xFF);
|
||||
query.extend_from_slice(device_id.as_bytes());
|
||||
query.push(0xFF);
|
||||
|
||||
let mut unused_algorithms = Vec::new();
|
||||
|
||||
self.db
|
||||
.fallbackkeyid_fallbackkey
|
||||
.stream_prefix(&query)
|
||||
.ignore_err()
|
||||
.ready_for_each(|((_, _, fallback_key_algorithm), (used, ..)): KeyVal| {
|
||||
if !used {
|
||||
unused_algorithms.push(fallback_key_algorithm);
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
unused_algorithms
|
||||
}
|
||||
|
||||
pub async fn add_device_keys(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
|
||||
Reference in New Issue
Block a user