///|
/// Whether the bundled native libdave runtime is available to this process.
pub fn available() -> Bool {
@ffi.available()
}
///|
/// Explain why native libdave support is unavailable, if it is unavailable.
pub fn unavailable_reason() -> String? {
if available() {
None
} else {
let reason = @ffi.unavailable_reason()
Some(if reason == "" { "libdave is unavailable" } else { reason })
}
}
///|
/// Return the maximum DAVE protocol version supported by the loaded libdave.
///
/// This returns zero when the native library is unavailable. It does not
/// identify the libdave ABI or release version.
pub fn max_supported_protocol_version() -> UInt16 {
@ffi.max_supported_protocol_version()
}
///|
fn require_available(operation : String) -> Unit raise DaveError {
if !available() {
let reason = unavailable_reason().unwrap_or("libdave is unavailable")
raise LibraryUnavailable(reason="\{reason} (required by \{operation})")
}
}
///|
fn validate_protocol_version(
operation : String,
protocol_version : UInt16,
) -> Unit raise DaveError {
require_available(operation)
let maximum = max_supported_protocol_version()
if protocol_version > maximum {
raise InvalidArgument(
operation~,
reason="protocol_version \{protocol_version} exceeds the supported maximum " +
maximum.to_string(),
)
}
}
///|
fn validate_initial_protocol_version(
operation : String,
protocol_version : UInt16,
) -> Unit raise DaveError {
if protocol_version == 0 {
raise InvalidArgument(
operation~,
reason="protocol_version must be greater than zero",
)
}
validate_protocol_version(operation, protocol_version)
}
///|
/// A stateful MLS session.
///
/// The native handle is owned by this value and released by a finalizer. A
/// copied `Session` aliases the same mutable native session, so calls on the
/// same session must be serialized by the application.
pub struct Session {
priv raw : @ffi.RawSession
priv self_user_id : Ref[UInt64]
priv has_group_context : Ref[Bool]
priv has_external_sender : Ref[Bool]
}
///|
/// A key ratchet derived from an established MLS session.
///
/// Installing a ratchet into an encryptor or decryptor copies its native key
/// state. The `KeyRatchet` therefore remains independently owned and may be
/// collected after installation.
pub struct KeyRatchet {
priv raw : @ffi.RawKeyRatchet
}
///|
fn Session::failure_detail(
self : Session,
fallback_reason : String,
) -> MlsFailure {
let (source, reason) = self.raw.last_failure()
MlsFailure::{
source: if source == "" {
"libdave"
} else {
source
},
reason: if reason == "" {
fallback_reason
} else {
reason
},
}
}
///|
fn Session::callback_failure(self : Session) -> MlsFailure? {
let (source, reason) = self.raw.last_failure()
if source == "" && reason == "" {
None
} else {
Some(MlsFailure::{
source: if source == "" {
"libdave"
} else {
source
},
reason: if reason == "" {
"libdave reported an unspecified MLS failure"
} else {
reason
},
})
}
}
///|
fn Session::check_native_status(
self : Session,
operation : String,
) -> Unit raise DaveError {
let status = self.raw.last_status()
if status != 0 {
let fallback_reason = "native status \{status}"
let failure = self.failure_detail(fallback_reason)
if status > 0 {
raise MlsOperationFailed(operation~, failure~)
}
raise NativeFailure(operation~, reason=failure.reason)
}
}
///|
fn Session::check_operation(
self : Session,
operation : String,
) -> Unit raise DaveError {
self.check_native_status(operation)
match self.callback_failure() {
Some(failure) => raise MlsOperationFailed(operation~, failure~)
None => ()
}
}
///|
/// Create and initialize an MLS session.
///
/// The official libdave v1.2.0 prebuilt C ABI has persistent keys disabled, so
/// each `Session` receives a new transient signing identity. Persisted native
/// authentication-session IDs are intentionally not exposed by this v0.1 API.
pub fn Session::new(
protocol_version~ : UInt16,
group_id~ : UInt64,
self_user_id~ : UInt64,
) -> Session raise DaveError {
let operation = "Session::new"
validate_initial_protocol_version(operation, protocol_version)
let raw = @ffi.RawSession::new()
let session = Session::{
raw,
self_user_id: Ref(self_user_id),
has_group_context: Ref(false),
has_external_sender: Ref(false),
}
if !raw.is_valid() {
raise NativeFailure(
operation~,
reason=session.failure_detail("failed to create a DAVE session").reason,
)
}
raw.init(protocol_version~, group_id~, self_user_id~)
session.check_operation(operation)
session.has_group_context.val = true
session
}
///|
/// Reset and initialize this session for a new group context.
///
/// This invokes libdave's session initialization again on the same native
/// handle. libdave retains a configured external sender and uses it to create
/// the new pending group state. The official v1.2.0 prebuilt C ABI also
/// generates a new transient signing identity on every reinitialization, so
/// verification state from the previous group must not be carried forward.
pub fn Session::reinitialize(
self : Session,
protocol_version~ : UInt16,
group_id~ : UInt64,
self_user_id~ : UInt64,
) -> Unit raise DaveError {
let operation = "Session::reinitialize"
validate_initial_protocol_version(operation, protocol_version)
// libdave begins Init by resetting its pending and established group state.
// Clear the mirror first so a later initialization failure cannot leave a
// stale readiness flag in this wrapper.
self.has_group_context.val = false
self.raw.init(protocol_version~, group_id~, self_user_id~)
self.check_operation(operation)
self.self_user_id.val = self_user_id
self.has_group_context.val = true
}
///|
/// Clear this session's MLS group state and group context.
///
/// libdave retains a configured external sender, so a later `reinitialize`
/// can create pending group state without setting the sender again.
pub fn Session::reset(self : Session) -> Unit raise DaveError {
self.has_group_context.val = false
self.raw.reset()
self.check_operation("Session::reset")
}
///|
/// Return the session's current protocol version.
pub fn Session::protocol_version(self : Session) -> UInt16 {
self.raw.protocol_version()
}
///|
/// Set the protocol version recorded by this session. Zero selects the
/// unencrypted protocol state; higher values must not exceed the loaded
/// library's supported maximum.
pub fn Session::set_protocol_version(
self : Session,
protocol_version~ : UInt16,
) -> Unit raise DaveError {
let operation = "Session::set_protocol_version"
validate_protocol_version(operation, protocol_version)
self.raw.set_protocol_version(protocol_version~)
self.check_operation(operation)
}
///|
/// Return the authenticator of the established MLS epoch, if one exists.
pub fn Session::last_epoch_authenticator(
self : Session,
) -> Bytes? raise DaveError {
let result = self.raw.last_epoch_authenticator()
self.check_operation("Session::last_epoch_authenticator")
if result.length() == 0 {
None
} else {
Some(result)
}
}
///|
/// Configure the serialized MLS external sender package.
///
/// The sender may only be changed while there is no pending or established
/// MLS group state. Call `reset` first when replacing a sender, then
/// `reinitialize` to create the new pending group state.
pub fn Session::set_external_sender(
self : Session,
external_sender : Bytes,
) -> Unit raise DaveError {
let operation = "Session::set_external_sender"
if self.has_group_context.val && self.has_external_sender.val {
raise InvalidState(
operation~,
reason="the external sender cannot be changed after MLS group state exists",
)
}
if external_sender.length() == 0 {
raise InvalidArgument(
operation~,
reason="external_sender must not be empty",
)
}
self.raw.set_external_sender(external_sender)
self.check_operation(operation)
self.has_external_sender.val = true
}
///|
/// Create a fresh, single-use marshalled MLS key package.
pub fn Session::key_package(self : Session) -> Bytes raise DaveError {
let result = self.raw.key_package()
self.check_operation("Session::key_package")
if result.length() == 0 {
raise MlsOperationFailed(
operation="Session::key_package",
failure=self.failure_detail("libdave returned an empty key package"),
)
}
result
}
///|
fn recognized_user_ids_with_self(
recognized_user_ids : ArrayView[UInt64],
self_user_id : UInt64,
) -> Array[UInt64] {
let result = []
for user_id in recognized_user_ids {
if !result.contains(user_id) {
result.push(user_id)
}
}
if !result.contains(self_user_id) {
result.push(self_user_id)
}
result
}
///|
fn require_nonempty_payload(
operation : String,
payload : Bytes,
) -> Unit raise DaveError {
if payload.length() == 0 {
raise InvalidArgument(operation~, reason="payload must not be empty")
}
}
///|
/// Process a complete libdave proposals payload.
///
/// The nonempty payload includes its leading revoke/append byte. On success libdave
/// returns one serialized commit followed by an optional serialized Welcome.
/// Processing requires pending or established MLS group state, normally
/// created by `set_external_sender` after initialization. A successful call
/// always returns a nonempty outbound commit/Welcome payload.
/// `recognized_user_ids` is the caller's explicit identity-recognition policy;
/// the wrapper adds this session's local user ID when it is absent.
pub fn Session::process_proposals(
self : Session,
proposals : Bytes,
recognized_user_ids~ : ArrayView[UInt64],
) -> Bytes raise DaveError {
let operation = "Session::process_proposals"
require_nonempty_payload(operation, proposals)
if !self.has_group_context.val || !self.has_external_sender.val {
raise InvalidState(
operation~,
reason="proposals require pending or established MLS group state",
)
}
let recognized = recognized_user_ids_with_self(
recognized_user_ids,
self.self_user_id.val,
)
let result = self.raw.process_proposals(proposals, recognized)
self.check_operation(operation)
if result.length() == 0 {
raise MlsOperationFailed(
operation~,
failure=self.failure_detail(
"libdave produced no commit/Welcome payload from the proposals",
),
)
}
result
}
///|
fn read_u64_be(record : Bytes) -> UInt64 {
(record[0].to_uint64() << 56) |
(record[1].to_uint64() << 48) |
(record[2].to_uint64() << 40) |
(record[3].to_uint64() << 32) |
(record[4].to_uint64() << 24) |
(record[5].to_uint64() << 16) |
(record[6].to_uint64() << 8) |
record[7].to_uint64()
}
///|
fn decode_roster_change(
record : Bytes,
operation : String,
) -> RosterChange raise DaveError {
if record.length() < 8 {
raise NativeFailure(
operation~,
reason="roster-change record is shorter than its 8-byte user ID",
)
}
let user_id = read_u64_be(record)
let signature_key = record[8:].to_owned()
if signature_key.length() == 0 {
Remove(user_id~)
} else {
Upsert(user_id~, signature_key~)
}
}
///|
fn result_tag(
raw : FixedArray[Bytes],
operation : String,
) -> Byte raise DaveError {
if raw.length() == 0 || raw[0].length() != 1 {
raise NativeFailure(operation~, reason="invalid native result tag")
}
raw[0][0]
}
///|
fn decode_commit_result(
raw : FixedArray[Bytes],
failure : MlsFailure,
) -> CommitResult raise DaveError {
let operation = "Session::process_commit"
match result_tag(raw, operation) {
0 => {
let changes = []
for index in 1.. {
if raw.length() != 1 {
raise NativeFailure(
operation~,
reason="Ignored commit carried roster data",
)
}
Ignored
}
2 => {
if raw.length() != 1 {
raise NativeFailure(
operation~,
reason="Failed commit carried roster data",
)
}
Failed(failure~)
}
tag =>
raise NativeFailure(
operation~,
reason="unknown native commit result tag \{tag}",
)
}
}
///|
fn decode_welcome_result(
raw : FixedArray[Bytes],
failure : MlsFailure,
) -> WelcomeResult raise DaveError {
let operation = "Session::process_welcome"
match result_tag(raw, operation) {
0 => {
let changes = []
for index in 1.. {
if raw.length() != 1 {
raise NativeFailure(
operation~,
reason="Failed Welcome carried roster data",
)
}
WelcomeResult::Failed(failure~)
}
tag =>
raise NativeFailure(
operation~,
reason="unknown native Welcome result tag \{tag}",
)
}
}
///|
/// Process an incoming MLS commit without collapsing its protocol outcome
/// into an exception. An empty serialized commit is rejected before FFI.
pub fn Session::process_commit(
self : Session,
commit : Bytes,
) -> CommitResult raise DaveError {
let operation = "Session::process_commit"
require_nonempty_payload(operation, commit)
let raw = self.raw.process_commit(commit)
self.check_native_status(operation)
decode_commit_result(
raw,
self.failure_detail("libdave rejected the MLS commit"),
)
}
///|
/// Process an incoming MLS Welcome without collapsing protocol rejection into
/// an exception. An applied result carries a roster delta: an `Upsert` has a
/// signature key and a `Remove` represents an empty native signature.
/// `recognized_user_ids` must contain the IDs that the caller recognizes for
/// this group; the wrapper adds this session's local user ID when it is absent.
/// An empty serialized Welcome is rejected before FFI.
pub fn Session::process_welcome(
self : Session,
welcome : Bytes,
recognized_user_ids~ : ArrayView[UInt64],
) -> WelcomeResult raise DaveError {
let operation = "Session::process_welcome"
require_nonempty_payload(operation, welcome)
let recognized = recognized_user_ids_with_self(
recognized_user_ids,
self.self_user_id.val,
)
let raw = self.raw.process_welcome(welcome, recognized)
self.check_native_status(operation)
decode_welcome_result(
raw,
self.failure_detail("libdave rejected the MLS Welcome"),
)
}
///|
/// Derive a media key ratchet for `user_id` from the established MLS epoch.
pub fn Session::key_ratchet(
self : Session,
user_id~ : UInt64,
) -> KeyRatchet? raise DaveError {
let raw = self.raw.key_ratchet(user_id~)
self.check_operation("Session::key_ratchet")
if raw.is_valid() {
Some(KeyRatchet::{ raw, })
} else {
None
}
}
///|
/// Compute a pairwise fingerprint for the currently established MLS group.
///
/// This proves only that the two signing keys in the current group produce the
/// same comparison value. With the official v1.2.0 prebuilt C ABI, signing
/// identities differ between concurrent `Session` values and rotate on every
/// `reinitialize`, so this is not persistent identity verification or DAVE
/// identity continuity. Discard the result when the session is reinitialized
/// or replaced. The DAVE fingerprint-format version is fixed to zero by this
/// API, as required by the canonical protocol.
///
/// The operation is deliberately synchronous and may block for an expensive
/// scrypt computation. Do not call it on a latency-sensitive cooperative event
/// loop.
pub fn Session::current_group_pairwise_fingerprint_blocking(
self : Session,
user_id~ : UInt64,
) -> Bytes raise DaveError {
let result = self.raw.pairwise_fingerprint(user_id~, fingerprint_version=0)
let status = self.raw.last_status()
if status != 0 || result.length() == 0 {
let failure = self.failure_detail("libdave returned an empty fingerprint")
raise FingerprintFailed(user_id~, reason=failure.reason)
}
result
}