mirror of
https://github.com/signalapp/libsignal.git
synced 2026-04-26 01:35:22 +02:00
259 lines
11 KiB
Swift
259 lines
11 KiB
Swift
//
|
||
// 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 {
|
||
let aci: Aci
|
||
let identityKey: IdentityKey
|
||
}
|
||
|
||
/// E.164 descriptor for key transparency requests.
|
||
public struct E164Info {
|
||
let e164: String
|
||
let unidentifiedAccessKey: Data
|
||
}
|
||
|
||
/// 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)
|
||
}
|
||
}
|
||
}
|