Broadcast-securejoin is working!!

This commit is contained in:
Hocuri
2025-08-01 16:32:11 +02:00
parent 24561cd256
commit 326deab025
11 changed files with 208 additions and 96 deletions

View File

@@ -45,6 +45,8 @@ pub enum QrObject {
/// Fingerprint of the contact key as scanned from the QR code.
fingerprint: String,
authcode: String,
/// The secret shared between all members,
/// used to symmetrically encrypt&decrypt messages.
shared_secret: String,
@@ -227,6 +229,7 @@ impl From<Qr> for QrObject {
grpid,
contact_id,
fingerprint,
authcode,
shared_secret,
} => {
let contact_id = contact_id.to_u32();
@@ -236,6 +239,7 @@ impl From<Qr> for QrObject {
grpid,
contact_id,
fingerprint,
authcode,
shared_secret,
}
}

View File

@@ -43,9 +43,9 @@ use crate::smtp::send_msg_to_smtp;
use crate::stock_str;
use crate::sync::{self, Sync::*, SyncData};
use crate::tools::{
IsNoneOrEmpty, SystemTime, buf_compress, create_broadcast_shared_secret, create_id,
create_outgoing_rfc724_mid, create_smeared_timestamp, create_smeared_timestamps, get_abs_path,
gm2local_offset, smeared_time, time, truncate_msg_text,
IsNoneOrEmpty, SystemTime, buf_compress, create_id, create_outgoing_rfc724_mid,
create_smeared_timestamp, create_smeared_timestamps, get_abs_path, gm2local_offset,
smeared_time, time, truncate_msg_text,
};
use crate::webxdc::StatusUpdateSerial;
use crate::{chatlist_events, imap};
@@ -1646,6 +1646,18 @@ impl Chat {
self.typ == Chattype::Mailinglist
}
/// Returns true if chat is an outgoing broadcast channel.
pub fn is_out_broadcast(&self) -> bool {
self.typ == Chattype::OutBroadcast
}
/// Returns true if the chat is a broadcast channel,
/// regardless of whether self is on the sending
/// or receiving side.
pub fn is_any_broadcast(&self) -> bool {
matches!(self.typ, Chattype::OutBroadcast | Chattype::InBroadcast)
}
/// Returns None if user can send messages to this chat.
///
/// Otherwise returns a reason useful for logging.
@@ -1726,7 +1738,7 @@ impl Chat {
match self.typ {
Chattype::Single | Chattype::OutBroadcast | Chattype::Mailinglist => Ok(true),
Chattype::Group => is_contact_in_chat(context, self.id, ContactId::SELF).await,
Chattype::InBroadcast => Ok(false),
Chattype::InBroadcast => Ok(true),
}
}
@@ -2916,13 +2928,18 @@ async fn prepare_send_msg(
CantSendReason::ContactRequest => {
// Allow securejoin messages, they are supposed to repair the verification.
// If the chat is a contact request, let the user accept it later.
msg.param.get_cmd() == SystemMessage::SecurejoinMessage
}
// Allow to send "Member removed" messages so we can leave the group/broadcast.
// Necessary checks should be made anyway before removing contact
// from the chat.
CantSendReason::NotAMember | CantSendReason::InBroadcast => {
msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup
CantSendReason::NotAMember => msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup,
CantSendReason::InBroadcast => {
matches!(
msg.param.get_cmd(),
SystemMessage::MemberRemovedFromGroup | SystemMessage::SecurejoinMessage
)
}
CantSendReason::MissingKey => msg
.param
@@ -3777,7 +3794,7 @@ pub async fn create_group_ex(
/// Returns the created chat's id.
pub async fn create_broadcast(context: &Context, chat_name: String) -> Result<ChatId> {
let grpid = create_id();
let secret = create_broadcast_shared_secret();
let secret = create_id();
create_broadcast_ex(context, Sync, grpid, chat_name, secret).await
}
@@ -3840,6 +3857,35 @@ pub(crate) async fn create_broadcast_ex(
Ok(chat_id)
}
pub(crate) async fn load_broadcast_shared_secret(
context: &Context,
chat_id: ChatId,
) -> Result<Option<String>> {
Ok(context
.sql
.query_get_value(
"SELECT secret FROM broadcasts_shared_secrets WHERE chat_id=?",
(chat_id,),
)
.await?)
}
pub(crate) async fn save_broadcast_shared_secret(
context: &Context,
chat_id: ChatId,
shared_secret: &str,
) -> Result<()> {
context
.sql
.execute(
"INSERT INTO broadcasts_shared_secrets (chat_id, secret) VALUES (?, ?)
ON CONFLICT(chat_id) DO UPDATE SET secret=excluded.chat_id",
(chat_id, shared_secret),
)
.await?;
Ok(())
}
/// Set chat contacts in the `chats_contacts` table.
pub(crate) async fn update_chat_contacts_table(
context: &Context,

View File

@@ -7,6 +7,7 @@ use crate::imex::{ImexMode, has_backup, imex};
use crate::message::{MessengerMessage, delete_msgs};
use crate::mimeparser::{self, MimeMessage};
use crate::receive_imf::receive_imf;
use crate::securejoin::get_securejoin_qr;
use crate::test_utils::{
AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, E2EE_INFO_MSGS, TestContext, TestContextManager,
TimeShiftFalsePositiveNote, sync,
@@ -2929,11 +2930,13 @@ async fn test_broadcast_channel_protected_listid() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
tcm.section("Create a broadcast channel with Bob, and send a message");
let alice_chat_id = create_broadcast(alice, "My Channel".to_string()).await?;
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
let qr = get_securejoin_qr(alice, Some(alice_chat_id)).await.unwrap();
tcm.exec_securejoin_qr(bob, alice, &qr).await;
let mut sent = alice.send_text(alice_chat_id, "Hi somebody").await;
assert!(!sent.payload.contains("List-ID"));

View File

@@ -15,7 +15,7 @@ use tokio::fs;
use crate::aheader::{Aheader, EncryptPreference};
use crate::blob::BlobObject;
use crate::chat::{self, Chat};
use crate::chat::{self, Chat, load_broadcast_shared_secret};
use crate::config::Config;
use crate::constants::ASM_SUBJECT;
use crate::constants::{Chattype, DC_FROM_HANDSHAKE};
@@ -231,6 +231,9 @@ impl MimeFactory {
// Do not encrypt messages to mailing lists.
encryption_keys = None;
} else if chat.is_out_broadcast() {
// Encrypt, but only symmetrically, not with the public keys.
encryption_keys = Some(Vec::new());
} else {
let email_to_remove = if msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup {
msg.param.get(Param::Arg)
@@ -563,8 +566,10 @@ impl MimeFactory {
// messages are auto-sent unlike usual unencrypted messages.
step == "vg-request-with-auth"
|| step == "vc-request-with-auth"
|| step == "vb-request-with-auth"
|| step == "vg-member-added"
|| step == "vc-contact-confirm"
// TODO possibly add vb-member-added here
}
}
@@ -1144,7 +1149,7 @@ impl MimeFactory {
};
let symmetric_key: Option<String> = match &self.loaded {
Loaded::Message { chat, .. } if chat.typ == Chattype::OutBroadcast => {
Loaded::Message { chat, .. } if chat.is_any_broadcast() => {
// If there is no symmetric key yet
// (because this is an old broadcast channel,
// created before we had symmetric encryption),
@@ -1152,13 +1157,7 @@ impl MimeFactory {
// Symmetric encryption exists since 2025-08;
// some time after that, we can think about requiring everyone
// to switch to symmetrically-encrypted broadcast lists.
context
.sql
.query_get_value(
"SELECT secret FROM broadcasts_shared_secrets WHERE chat_id=?",
(chat.id,),
)
.await?
load_broadcast_shared_secret(context, chat.id).await?
}
_ => None,
};
@@ -1515,7 +1514,10 @@ impl MimeFactory {
let param2 = msg.param.get(Param::Arg2).unwrap_or_default();
if !param2.is_empty() {
headers.push((
if step == "vg-request-with-auth" || step == "vc-request-with-auth" {
if step == "vg-request-with-auth"
|| step == "vc-request-with-auth"
|| step == "vb-request-with-auth"
{
"Secure-Join-Auth"
} else {
"Secure-Join-Invitenumber"

View File

@@ -96,6 +96,8 @@ pub enum Qr {
fingerprint: Fingerprint,
authcode: String,
shared_secret: String,
},
@@ -396,7 +398,7 @@ pub fn format_backup(qr: &Qr) -> Result<String> {
/// scheme: `OPENPGP4FPR:FINGERPRINT#a=ADDR&n=NAME&i=INVITENUMBER&s=AUTH`
/// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR&g=GROUPNAME&x=GROUPID&i=INVITENUMBER&s=AUTH`
/// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR&g=BROADCAST_NAME&x=BROADCAST_ID&b=BROADCAST_SHARED_SECRET`
/// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR&g=BROADCAST_NAME&x=BROADCAST_ID&s=AUTH&b=BROADCAST_SHARED_SECRET`
/// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR`
async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
let payload = qr
@@ -474,7 +476,9 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
None
};
if let (Some(addr), Some(invitenumber), Some(authcode)) = (&addr, invitenumber, authcode) {
if let (Some(addr), Some(invitenumber), Some(authcode)) =
(&addr, invitenumber, authcode.clone())
{
let addr = ContactAddress::new(addr)?;
let (contact_id, _) = Contact::add_or_lookup_ex(
context,
@@ -545,8 +549,13 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
authcode,
})
}
} else if let (Some(addr), Some(broadcast_name), Some(grpid), Some(shared_secret)) =
(&addr, grpname, grpid, broadcast_shared_secret)
} else if let (
Some(addr),
Some(broadcast_name),
Some(grpid),
Some(authcode),
Some(shared_secret),
) = (&addr, grpname, grpid, authcode, broadcast_shared_secret)
{
// This is a broadcast channel invite link.
// TODO code duplication with the previous block
@@ -567,6 +576,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
grpid,
contact_id,
fingerprint,
authcode,
shared_secret,
})
} else if let Some(addr) = addr {

View File

@@ -44,7 +44,7 @@ use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on
use crate::simplify;
use crate::stock_str;
use crate::sync::Sync::*;
use crate::tools::{self, buf_compress, create_broadcast_shared_secret, remove_subject_prefix};
use crate::tools::{self, buf_compress, create_id, remove_subject_prefix};
use crate::{chatlist_events, ensure_and_debug_assert, ensure_and_debug_assert_eq, location};
use crate::{contact, imap};
@@ -1566,7 +1566,7 @@ async fn do_chat_assignment(
} else {
let name =
compute_mailinglist_name(mailinglist_header, &listid, mime_parser);
let secret = create_broadcast_shared_secret();
let secret = create_id();
chat::create_broadcast_ex(context, Nosync, listid, name, secret).await?
},
);

View File

@@ -4,7 +4,10 @@ use anyhow::{Context as _, Error, Result, bail, ensure};
use deltachat_contact_tools::ContactAddress;
use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus, get_chat_id_by_grpid};
use crate::chat::{
self, Chat, ChatId, ChatIdBlocked, ProtectionStatus, get_chat_id_by_grpid,
load_broadcast_shared_secret,
};
use crate::chatlist_events;
use crate::config::Config;
use crate::constants::{Blocked, Chattype, NON_ALPHANUMERIC_WITHOUT_DOT};
@@ -46,9 +49,9 @@ fn inviter_progress(context: &Context, contact_id: ContactId, progress: usize) {
/// Generates a Secure Join QR code.
///
/// With `group` set to `None` this generates a setup-contact QR code, with `group` set to a
/// [`ChatId`] generates a join-group QR code for the given chat.
pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Result<String> {
/// With `chat` set to `None` this generates a setup-contact QR code, with `chat` set to a
/// [`ChatId`] generates a join-group/join-broadcast-channel QR code for the given chat.
pub async fn get_securejoin_qr(context: &Context, chat: Option<ChatId>) -> Result<String> {
/*=======================================================
==== Alice - the inviter side ====
==== Step 1 in "Setup verified contact" protocol ====
@@ -56,12 +59,13 @@ pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Resu
ensure_secret_key_exists(context).await.ok();
let chat = match group {
let chat = match chat {
Some(id) => {
let chat = Chat::load_from_db(context, id).await?;
ensure!(
chat.typ == Chattype::Group,
"Can't generate SecureJoin QR code for 1:1 chat {id}"
chat.typ == Chattype::Group || chat.typ == Chattype::OutBroadcast,
"Can't generate SecureJoin QR code for chat {id} of type {}",
chat.typ
);
if chat.grpid.is_empty() {
let err = format!("Can't generate QR code, chat {id} is a email thread");
@@ -94,24 +98,44 @@ pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Resu
utf8_percent_encode(&self_name, NON_ALPHANUMERIC_WITHOUT_DOT).to_string();
let qr = if let Some(chat) = chat {
// parameters used: a=g=x=i=s=
let group_name = chat.get_name();
let group_name_urlencoded = utf8_percent_encode(group_name, NON_ALPHANUMERIC).to_string();
if sync_token {
context
.sync_qr_code_tokens(Some(chat.grpid.as_str()))
.await?;
context.scheduler.interrupt_inbox().await;
if chat.typ == Chattype::OutBroadcast {
let broadcast_name = chat.get_name();
let broadcast_name_urlencoded =
utf8_percent_encode(broadcast_name, NON_ALPHANUMERIC).to_string();
let broadcast_secret = load_broadcast_shared_secret(context, chat.id)
.await?
.context("Could not find broadcast secret")?;
format!(
"https://i.delta.chat/#{}&a={}&g={}&x={}&s={}&b={}",
fingerprint.hex(),
self_addr_urlencoded,
&broadcast_name_urlencoded,
&chat.grpid,
&auth,
broadcast_secret
)
} else {
// parameters used: a=g=x=i=s=
let group_name = chat.get_name();
let group_name_urlencoded =
utf8_percent_encode(group_name, NON_ALPHANUMERIC).to_string();
if sync_token {
context
.sync_qr_code_tokens(Some(chat.grpid.as_str()))
.await?;
context.scheduler.interrupt_inbox().await;
}
format!(
"https://i.delta.chat/#{}&a={}&g={}&x={}&i={}&s={}",
fingerprint.hex(),
self_addr_urlencoded,
&group_name_urlencoded,
&chat.grpid,
&invitenumber,
&auth,
)
}
format!(
"https://i.delta.chat/#{}&a={}&g={}&x={}&i={}&s={}",
fingerprint.hex(),
self_addr_urlencoded,
&group_name_urlencoded,
&chat.grpid,
&invitenumber,
&auth,
)
} else {
// parameters used: a=n=i=s=
if sync_token {
@@ -266,9 +290,9 @@ pub(crate) async fn handle_securejoin_handshake(
info!(context, "Received secure-join message {step:?}.");
let join_vg = step.starts_with("vg-");
if !matches!(step, "vg-request" | "vc-request") {
// TODO talk with link2xt about whether we need to protect against this identity-misbinding attack,
// and if so, how
if !matches!(step, "vg-request" | "vc-request" | "vb-request-with-auth") {
let mut self_found = false;
let self_fingerprint = load_self_public_key(context).await?.dc_fingerprint();
for (addr, key) in &mime_message.gossiped_keys {
@@ -338,7 +362,7 @@ pub(crate) async fn handle_securejoin_handshake(
========================================================*/
bob::handle_auth_required(context, mime_message).await
}
"vg-request-with-auth" | "vc-request-with-auth" => {
"vg-request-with-auth" | "vc-request-with-auth" | "vb-request-with-auth" => {
/*==========================================================
==== Alice - the inviter side ====
==== Steps 5+6 in "Setup verified contact" protocol ====
@@ -399,7 +423,7 @@ pub(crate) async fn handle_securejoin_handshake(
ContactId::scaleup_origin(context, &[contact_id], Origin::SecurejoinInvited).await?;
// for setup-contact, make Alice's one-to-one chat with Bob visible
// (secure-join-information are shown in the group chat)
if !join_vg {
if step.starts_with("vc-") {
ChatId::create_for_contact(context, contact_id).await?;
}
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
@@ -500,6 +524,7 @@ pub(crate) async fn handle_securejoin_handshake(
/// we know that we are Alice (inviter-observer)
/// that just marked peer (Bob) as verified
/// in response to correct vc-request-with-auth message.
// TODO here I may be able to fix some multi-device things
pub(crate) async fn observe_securejoin_on_other_device(
context: &Context,
mime_message: &MimeMessage,

View File

@@ -4,7 +4,9 @@ use anyhow::{Context as _, Result};
use super::HandshakeMessage;
use super::qrinvite::QrInvite;
use crate::chat::{self, ChatId, ProtectionStatus, is_contact_in_chat};
use crate::chat::{
self, ChatId, ProtectionStatus, is_contact_in_chat, save_broadcast_shared_secret,
};
use crate::constants::{Blocked, Chattype};
use crate::contact::Origin;
use crate::context::Context;
@@ -56,8 +58,43 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
ContactId::scaleup_origin(context, &[invite.contact_id()], Origin::SecurejoinJoined).await?;
context.emit_event(EventType::ContactsChanged(None));
// Now start the protocol and initialise the state.
{
if let QrInvite::Broadcast { shared_secret, .. } = &invite {
// TODO this causes some performance penalty because joining_chat_id is used again below,
// but maybe it's fine
let broadcast_chat_id = joining_chat_id(context, &invite, chat_id).await?;
// TODO save the secret to the second device
save_broadcast_shared_secret(context, broadcast_chat_id, shared_secret).await?;
if verify_sender_by_fingerprint(context, invite.fingerprint(), invite.contact_id()).await? {
info!(context, "Using fast securejoin with symmetric encryption");
// The message has to be sent into the broadcast chat, rather than the 1:1 chat,
// so that it will be symmetrically encrypted
send_handshake_message(
context,
&invite,
broadcast_chat_id,
BobHandshakeMsg::RequestWithAuth,
)
.await?;
// Mark 1:1 chat as verified already.
chat_id
.set_protection(
context,
ProtectionStatus::Protected,
time(),
Some(invite.contact_id()),
)
.await?;
context.emit_event(EventType::SecurejoinJoinerProgress {
contact_id: invite.contact_id(),
progress: JoinerProgress::RequestWithAuthSent.to_usize(),
});
}
} else {
// Start the original (non-broadcast) protocol and initialise the state.
let has_key = context
.sql
.exists(
@@ -115,22 +152,22 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
Ok(group_chat_id)
}
QrInvite::Broadcast { .. } => {
// For a secure-join we need to create the group and add the contact. The group will
// only become usable once the protocol is finished.
let group_chat_id = joining_chat_id(context, &invite, chat_id).await?;
if !is_contact_in_chat(context, group_chat_id, invite.contact_id()).await? {
// TODO code duplication with previous block
let broadcast_chat_id = joining_chat_id(context, &invite, chat_id).await?;
if !is_contact_in_chat(context, broadcast_chat_id, invite.contact_id()).await? {
chat::add_to_chat_contacts_table(
context,
time(),
group_chat_id,
broadcast_chat_id,
&[invite.contact_id()],
)
.await?;
}
// TODO this message should be translatable:
let msg = "You were invited to join this channel. Waiting for the channel owner's device to reply…";
chat::add_info_msg(context, group_chat_id, msg, time()).await?;
Ok(group_chat_id)
chat::add_info_msg(context, broadcast_chat_id, msg, time()).await?;
Ok(broadcast_chat_id)
}
QrInvite::Contact { .. } => {
// For setup-contact the BobState already ensured the 1:1 chat exists because it
@@ -318,7 +355,7 @@ pub(crate) async fn send_handshake_message(
pub(crate) enum BobHandshakeMsg {
/// vc-request or vg-request
Request,
/// vc-request-with-auth or vg-request-with-auth
/// vc-request-with-auth, vg-request-with-auth, or vb-request-with-auth
RequestWithAuth,
}
@@ -342,14 +379,14 @@ impl BobHandshakeMsg {
Self::Request => match invite {
QrInvite::Contact { .. } => "vc-request",
QrInvite::Group { .. } => "vg-request",
QrInvite::Broadcast { .. } => "broadcast-request",
QrInvite::Broadcast { .. } => {
panic!("There is no request-with-auth for broadcasts")
} // TODO remove panic
},
Self::RequestWithAuth => match invite {
QrInvite::Contact { .. } => "vc-request-with-auth",
QrInvite::Group { .. } => "vg-request-with-auth",
QrInvite::Broadcast { .. } => {
panic!("There is no request-with-auth for broadcasts")
} // TODO remove panic
QrInvite::Broadcast { .. } => "vb-request-with-auth",
},
}
}

View File

@@ -30,10 +30,11 @@ pub enum QrInvite {
authcode: String,
},
Broadcast {
broadcast_name: String,
grpid: String,
contact_id: ContactId,
fingerprint: Fingerprint,
broadcast_name: String,
grpid: String,
authcode: String,
shared_secret: String,
},
}
@@ -71,8 +72,9 @@ impl QrInvite {
/// The `AUTH` code of the setup-contact/secure-join protocol.
pub fn authcode(&self) -> &str {
match self {
Self::Contact { authcode, .. } | Self::Group { authcode, .. } => authcode,
Self::Broadcast { .. } => panic!("broadcast invite has no authcode"), // TODO panic
Self::Contact { authcode, .. }
| Self::Group { authcode, .. }
| Self::Broadcast { authcode, .. } => authcode,
}
}
}
@@ -113,15 +115,17 @@ impl TryFrom<Qr> for QrInvite {
grpid,
contact_id,
fingerprint,
authcode,
shared_secret,
} => Ok(QrInvite::Broadcast {
broadcast_name,
grpid,
contact_id,
fingerprint,
authcode,
shared_secret,
}),
_ => bail!("Unsupported QR type"),
_ => bail!("Unsupported QR type: {qr:?}"),
}
}
}

View File

@@ -229,15 +229,15 @@ impl TestContextManager {
pub async fn exec_securejoin_qr(
&self,
scanner: &TestContext,
scanned: &TestContext,
inviter: &TestContext,
qr: &str,
) -> ChatId {
let chat_id = join_securejoin(&scanner.ctx, qr).await.unwrap();
loop {
if let Some(sent) = scanner.pop_sent_msg_opt(Duration::ZERO).await {
scanned.recv_msg_opt(&sent).await;
} else if let Some(sent) = scanned.pop_sent_msg_opt(Duration::ZERO).await {
inviter.recv_msg_opt(&sent).await;
} else if let Some(sent) = inviter.pop_sent_msg_opt(Duration::ZERO).await {
scanner.recv_msg_opt(&sent).await;
} else {
break;

View File

@@ -300,25 +300,6 @@ pub(crate) fn create_id() -> String {
base64::engine::general_purpose::URL_SAFE.encode(arr)
}
/// Generate a shared secret for a broadcast channel, consisting of 64 characters..
///
/// The string generated by this function has 384 bits of entropy
/// and is returned as 64 Base64 characters, each containing 6 bits of entropy.
/// 384 is chosen because it is sufficiently secure
/// (larger than AES-128 keys used for message encryption)
/// and divides both by 8 (byte size) and 6 (number of bits in a single Base64 character).
// TODO ask someone what a good size would be here - also, not sure whether the AES-128 thing is true
pub(crate) fn create_broadcast_shared_secret() -> String {
// ThreadRng implements CryptoRng trait and is supposed to be cryptographically secure.
let mut rng = thread_rng();
// Generate 384 random bits.
let mut arr = [0u8; 48];
rng.fill(&mut arr[..]);
base64::engine::general_purpose::URL_SAFE.encode(arr)
}
/// Returns true if given string is a valid ID.
///
/// All IDs generated with `create_id()` should be considered valid.