Files
libsignal/swift/Sources/LibSignalClient/KeyTransparency.swift
2026-01-29 11:49:59 -05:00

269 lines
12 KiB
Swift
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// Copyright 2025 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import SignalFfi
public enum KeyTransparency {
/// Protocol for a local persistent key transparency data store.
///
/// Contents of the store are opaque to the client and are only supposed to be
/// used by the ``Client``.
///
public protocol Store {
func getLastDistinguishedTreeHead() async -> Data?
func setLastDistinguishedTreeHead(to: Data) async
func getAccountData(for aci: Aci) async -> Data?
func setAccountData(_ data: Data, for aci: Aci) async
}
/// ACI descriptor for key transparency requests.
public struct AciInfo {
public let aci: Aci
public let identityKey: IdentityKey
public init(aci: Aci, identityKey: IdentityKey) {
self.aci = aci
self.identityKey = identityKey
}
}
/// E.164 descriptor for key transparency requests.
public struct E164Info {
public let e164: String
public let unidentifiedAccessKey: Data
public init(e164: String, unidentifiedAccessKey: Data) {
self.e164 = e164
self.unidentifiedAccessKey = unidentifiedAccessKey
}
}
/// Mode of the monitor operation.
///
/// If the newer version of account data is found in the key transparency
/// log, self-monitor will terminate with an error, but monitor for other
/// account will fall back to a full search and update the locally stored
/// data.
public enum MonitorMode {
case `self`
case other
}
/// Typed API to access the key transparency subsystem using an existing
/// unauthenticated chat connection.
///
/// Unlike ``UnauthenticatedChatConnection``, the client does
/// not export "raw" send/receive APIs, and instead uses them internally to
/// implement high-level key transparency operations.
///
/// Instances should be obtained by using the
/// ``UnauthenticatedChatConnection/keyTransparencyClient`` property.
///
/// Example usage:
///
/// ```swift
/// let network = Net(
/// env: .staging,
/// userAgent: "key-transparency-example"
/// )
///
/// let chat = try await network.connectUnauthenticatedChat()
/// chat.start(listener: MyChatListener())
///
/// // Successful completion means the search succeeded with no further steps required.
/// try await chat.keyTransparencyClient.search(
/// account: myAciInfo,
/// e164: myE164Info,
/// store: store
/// )
/// ```
public class Client {
private let chatConnection: UnauthenticatedChatConnection
private let asyncContext: TokioAsyncContext
private let environment: Net.Environment
internal init(
chatConnection: UnauthenticatedChatConnection,
asyncContext: TokioAsyncContext,
environment: Net.Environment
) {
self.chatConnection = chatConnection
self.asyncContext = asyncContext
self.environment = environment
}
/// Search for account information in the key transparency tree.
///
/// - Parameters:
/// - aciInfo: ACI identifying information.
/// - e164Info: E.164 identifying information. Optional.
/// - usernameHash: Hash of the username. Optional.
/// - store: Local key transparency storage. It will be queried for both
/// the account data and the latest distinguished tree head before sending the
/// server request and, if the request succeeds, will be updated with the
/// search operation results.
/// - Throws:
/// - ``SignalError/keyTransparencyError`` for errors related to key transparency logic, which
/// includes missing required fields in the serialized data. Retrying the search without
/// changing any of the arguments (including the state of the store) is unlikely to yield a
/// different result.
/// - ``SignalError/keyTransparencyVerificationFailed`` when it fails to
/// verify the data in key transparency server response, such as an incorrect proof or a
/// wrong signature.
/// - ``SignalError/rateLimitedError(retryAfter:message:)`` if the server is rate limiting
/// this client. This is **retryable** after waiting the designated delay.
/// - ``SignalError/connectionFailed(_:)``, ``SignalError/ioError(_:)``, or
/// ``SignalError/webSocketError(_:)`` for networking failures before and during
/// communication with the server. These can be **automatically retried** (backoff
/// recommended).
/// - Other ``SignalError``s for networking issues. These can be manually
/// retried, but some may indicate a possible bug in libsignal.
///
/// Completes successfully if the search succeeds and the local state has been
/// updated to reflect the latest changes. If the operation fails, the UI should
/// be updated to notify the user of the failure.
public func search(
account aciInfo: AciInfo,
e164 e164Info: E164Info? = nil,
usernameHash: Data? = nil,
store: some Store
) async throws {
let e164 = e164Info?.e164
let uak = e164Info?.unidentifiedAccessKey
let accountData = await store.getAccountData(for: aciInfo.aci)
let distinguished = try await self.updateDistinguished(store)
let bytes = try await self.asyncContext.invokeAsyncFunction { promise, tokioContext in
try! withAllBorrowed(
self.chatConnection,
aciInfo.aci,
aciInfo.identityKey.publicKey,
uak,
usernameHash,
accountData,
distinguished
) { chatHandle, aciBytes, identityKeyHandle, uakBytes, hashBytes, accDataBytes, distinguishedBytes in
signal_key_transparency_search(
promise,
tokioContext.const(),
self.environment.rawValue,
chatHandle.const(),
aciBytes,
identityKeyHandle.const(),
e164,
uakBytes,
hashBytes,
accDataBytes,
distinguishedBytes
)
}
}
await store.setAccountData(Data(consuming: bytes), for: aciInfo.aci)
}
/// Perform a monitor operation for an account previously searched for.
///
/// - Parameters:
/// - mode: Mode of the monitor operation. See ``MonitorMode``.
/// - aciInfo: ACI identifying information.
/// - e164Info: E.164 identifying information. Optional.
/// - usernameHash: Hash of the username. Optional.
/// - store: Local key transparency storage. It will be queried for both
/// the account data and the latest distinguished tree head before sending the
/// server request and, if the request succeeds, will be updated with the
/// search operation results.
/// - Throws:
/// - ``SignalErrorrkeyTransparencyError`` for errors related to key transparency logic, which
/// includes missing required fields in the serialized data. Retrying the search without
/// changing any of the arguments (including the state of the store) is unlikely to yield a
/// different result.
/// - ``SignalError/keyTransparencyVerificationFailed`` when it fails to
/// verify the data in key transparency server response, such as an incorrect proof or a
/// wrong signature. This is also the error thrown when new version
/// of account data is found in the key transparency log when
/// self-monitoring. See ``MonitorMode``.
/// - ``SignalError/rateLimitedError(retryAfter:message:)`` if the server is rate limiting
/// this client. This is **retryable** after waiting the designated delay.
/// - ``SignalError/connectionFailed(_:)``, ``SignalError/ioError(_:)``, or
/// ``SignalError/webSocketError(_:)`` for networking failures before and during
/// communication with the server. These can be **automatically retried** (backoff
/// recommended).
/// - Other ``SignalError``s for networking issues. These can be manually
/// retried, but some may indicate a possible bug in libsignal.
///
///
/// Completes successfully if the search succeeds and the local state has been
/// updated to reflect the latest changes. If the operation fails, the UI should
/// be updated to notify the user of the failure.
public func monitor(
for mode: MonitorMode,
account aciInfo: AciInfo,
e164 e164Info: E164Info? = nil,
usernameHash: Data? = nil,
store: some Store
) async throws {
let e164 = e164Info?.e164
let uak = e164Info?.unidentifiedAccessKey
let accountData = await store.getAccountData(for: aciInfo.aci)
let distinguished = try await self.updateDistinguished(store)
let bytes = try await self.asyncContext.invokeAsyncFunction { promise, tokioContext in
try! withAllBorrowed(
self.chatConnection,
aciInfo.aci,
aciInfo.identityKey.publicKey,
uak,
usernameHash,
accountData,
distinguished
) { chatHandle, aciBytes, identityKeyHandle, uakBytes, hashBytes, accDataBytes, distinguishedBytes in
signal_key_transparency_monitor(
promise,
tokioContext.const(),
self.environment.rawValue,
chatHandle.const(),
aciBytes,
identityKeyHandle.const(),
e164,
uakBytes,
hashBytes,
accDataBytes,
distinguishedBytes,
mode == .self
)
}
}
await store.setAccountData(Data(consuming: bytes), for: aciInfo.aci)
}
private func updateDistinguished(_ store: some Store) async throws -> Data {
let knownDistinguished = await store.getLastDistinguishedTreeHead()
let latestDistinguished = try await getDistinguished(knownDistinguished)
await store.setLastDistinguishedTreeHead(to: latestDistinguished)
return latestDistinguished
}
internal func getDistinguished(
_ distinguished: Data? = nil
) async throws -> Data {
let bytes = try await self.asyncContext.invokeAsyncFunction { promise, tokioContext in
try! withAllBorrowed(self.chatConnection, distinguished) { chatHandle, distinguishedBytes in
signal_key_transparency_distinguished(
promise,
tokioContext.const(),
self.environment.rawValue,
chatHandle.const(),
distinguishedBytes
)
}
}
return Data(consuming: bytes)
}
}
}