Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,87 @@ called out explicitly even when nothing else did.
release notes, so a version with no entry here does not release. Write the entry in the same PR that
syncs the contract, while the diff is still in front of you.

## 0.12.0

Synced to [`flipcash2-protobuf-api@9ebf55fe`](https://github.com/code-payments/flipcash2-protobuf-api/commit/9ebf55fef834c1a47ae995ae22cad28d79080f2c),
picking up [#117](https://github.com/code-payments/flipcash2-protobuf-api/pull/117) through
[#122](https://github.com/code-payments/flipcash2-protobuf-api/pull/122). `blob.v1`, `chat.v1`,
`messaging.v1` and `push.v1` moved.

Most of this release is end-to-end encryption for DMs: an encrypted content type, encrypted blob
uploads for DM media, and a per-chat flag for migrating DMs over. It is additive on the wire, but
one existing Swift accessor is gone and pushes can now arrive without the message, so the upgrade is
not free on iOS. Both are under Upgrading.

### Added

End-to-end encrypted DMs, in `messaging.v1`:

- `EncryptedContent`, a new `Content.type` case (`encrypted = 7`), carrying `scheme` (field 1),
a 24-byte `nonce` (field 2) and `ciphertext` (field 3, up to 17408 bytes including the 16-byte
tag). The plaintext is a serialized `Content` holding a `TextContent`, a `MediaContent`, or a
`ReplyContent` of either. `EncryptedContent.Scheme` has `UNKNOWN = 0` and
`X25519_XCHACHA20POLY1305 = 1`. The proto comment on `EncryptedContent` specifies the key
derivation, AAD and blob format in full; follow it rather than this summary.
- `SendMessageResponse.Result.ENCRYPTION_NOT_ALLOWED = 2` and
`EditMessageResponse.Result.ENCRYPTION_NOT_ALLOWED = 5`, returned when `EncryptedContent` is sent
to a chat that is not a DM.

Encrypted blobs, in `blob.v1`:

- `InitiateExternalUploadRequest.end_to_end_encrypted_for`, a oneof whose only case is
`chat = 4` (`common.v1.ChatId`). The caller must be a member of that DM. `mime_type` must be
`application/octet-stream`, and the server checks only the size.
- `EncryptedBlobMetadata`, a new empty message and a new `BlobMetadata.kind` case
(`encrypted = 5`), marking a blob uploaded that way.
- `UploadPolicy.encrypted` (field 4) and the new `EncryptedConstraints`, with `max_size_bytes`
(field 1, the only enforced limit) and advisory `image` bounds (field 2). Unset when the caller
may not upload encrypted blobs.

Chat metadata, in `chat.v1.Metadata`:

- `creator` (field 13, `common.v1.UserId`), set for group chats only.
- `use_e2ee` (field 100, `bool`), true when clients should send new content in this DM as
`EncryptedContent`. Always false for group chats. It is transitional and will be deprecated once
E2EE launches. The Swift accessor is `useE2Ee`.

Push, in `push.v1.ChatMetadata`:

- `message_id` (field 5, `messaging.v1.MessageId`), sent in place of the full message when the
message would push the payload over the 4KB FCM/APNs limit.

### Changed

- `ChatMetadata.message` (field 3) now sits inside a new `message_ref` oneof alongside
`message_id`. The field number and type are unchanged, so this is wire-compatible. In Swift the
generated `hasMessage` and `clearMessage()` are gone; read `messageRef` instead. Kotlin keeps
`hasMessage()` and gains `getMessageRefCase()`.
- `ChatMetadata.sending_user_id` is no longer deprecated. It is set whether the push carries the
message or only its ID.
- `Chat.GetChat` documents an unauthenticated read: with `auth` unset, the caller gets the chat's
public view, and `view_mode` must be `REDACTED`. Any other mode, or a DM, is `DENIED`. No field
changed.

### Upgrading

**A chat push may no longer carry the message.** This applies to every client, including one that
never upgrades: the server now omits the message from a push for a long message and sends
`message_id` instead, which an older client sees as `message` simply unset. The push's title and
body are still set, so the notification can be shown either way. A client that needs the message,
to decrypt it or to store it for a muted chat, fetches it with `Messaging.GetMessage` using
`message_id` and the chat ID from `Payload.navigation`.

**`EncryptedContent` needs handling before a DM turns on `use_e2ee`.** The server cannot read,
moderate or preview it. A client that cannot decrypt a message, or decrypts a type outside the
allowed three, renders it as unsupported rather than failing.

### Unchanged

Nothing was renumbered. Both new `Result` cases are appended after the last existing case (`DENIED`
and `CONFLICT` respectively), so no positional `rawValue` mapping shifts. `Content.encrypted` and
`BlobMetadata.encrypted` are appended to their oneofs, and every new field takes a free number. No
service, RPC, message or enum case was removed. Everything else in the diff is comments.

## 0.11.0

Synced to [`flipcash2-protobuf-api@4ccbbe43`](https://github.com/code-payments/flipcash2-protobuf-api/commit/4ccbbe43197ec6cc2d7a3f0681a73f799cdcfb46),
Expand Down
2 changes: 1 addition & 1 deletion Sources/Flipcash2ClientProtocol/ContractInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

public enum Flipcash2ContractInfo {
public static let version = "0.11.0-dev"
public static let protoCommit = "4ccbbe43197ec6cc2d7a3f0681a73f799cdcfb46"
public static let protoCommit = "9ebf55fef834c1a47ae995ae22cad28d79080f2c"

public static var isLocal: Bool { protoCommit == localSentinel }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,10 @@ extension Flipcash_Blob_V1_BlobStorage {
/// > BlobStorage manages direct-to-storage uploads and authorized, time-limited reads
/// > of the bytes behind MediaItem renditions (and other blobs). Clients upload bytes
/// > straight to object storage via a presigned target — the server never proxies
/// > them — and all blob metadata is server-derived from the stored bytes.
/// > them — and all blob metadata is server-derived from the stored bytes. The
/// > exception is end-to-end encrypted blobs (see
/// > InitiateExternalUploadRequest.end_to_end_encrypted_for), whose bytes the
/// > server cannot read.
public protocol ClientProtocol: Sendable {
/// Call the "GetUploadPolicy" method.
///
Expand Down Expand Up @@ -133,7 +136,8 @@ extension Flipcash_Blob_V1_BlobStorage {
/// >
/// > InitiateExternalUpload reserves a BlobId and returns a short-lived presigned
/// > target the client uploads the bytes to directly. Clients only ever upload
/// > ORIGINALs; the server derives any additional renditions itself.
/// > ORIGINALs; the server derives any additional renditions itself, except
/// > for end-to-end encrypted blobs, which have none.
///
/// - Parameters:
/// - request: A request containing a single `Flipcash_Blob_V1_InitiateExternalUploadRequest` message.
Expand Down Expand Up @@ -219,7 +223,10 @@ extension Flipcash_Blob_V1_BlobStorage {
/// > BlobStorage manages direct-to-storage uploads and authorized, time-limited reads
/// > of the bytes behind MediaItem renditions (and other blobs). Clients upload bytes
/// > straight to object storage via a presigned target — the server never proxies
/// > them — and all blob metadata is server-derived from the stored bytes.
/// > them — and all blob metadata is server-derived from the stored bytes. The
/// > exception is end-to-end encrypted blobs (see
/// > InitiateExternalUploadRequest.end_to_end_encrypted_for), whose bytes the
/// > server cannot read.
public struct Client<Transport>: ClientProtocol where Transport: GRPCCore.ClientTransport {
private let client: GRPCCore.GRPCClient<Transport>

Expand Down Expand Up @@ -275,7 +282,8 @@ extension Flipcash_Blob_V1_BlobStorage {
/// >
/// > InitiateExternalUpload reserves a BlobId and returns a short-lived presigned
/// > target the client uploads the bytes to directly. Clients only ever upload
/// > ORIGINALs; the server derives any additional renditions itself.
/// > ORIGINALs; the server derives any additional renditions itself, except
/// > for end-to-end encrypted blobs, which have none.
///
/// - Parameters:
/// - request: A request containing a single `Flipcash_Blob_V1_InitiateExternalUploadRequest` message.
Expand Down Expand Up @@ -426,7 +434,8 @@ extension Flipcash_Blob_V1_BlobStorage.ClientProtocol {
/// >
/// > InitiateExternalUpload reserves a BlobId and returns a short-lived presigned
/// > target the client uploads the bytes to directly. Clients only ever upload
/// > ORIGINALs; the server derives any additional renditions itself.
/// > ORIGINALs; the server derives any additional renditions itself, except
/// > for end-to-end encrypted blobs, which have none.
///
/// - Parameters:
/// - request: A request containing a single `Flipcash_Blob_V1_InitiateExternalUploadRequest` message.
Expand Down Expand Up @@ -565,7 +574,8 @@ extension Flipcash_Blob_V1_BlobStorage.ClientProtocol {
/// >
/// > InitiateExternalUpload reserves a BlobId and returns a short-lived presigned
/// > target the client uploads the bytes to directly. Clients only ever upload
/// > ORIGINALs; the server derives any additional renditions itself.
/// > ORIGINALs; the server derives any additional renditions itself, except
/// > for end-to-end encrypted blobs, which have none.
///
/// - Parameters:
/// - message: request message to send.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,59 @@ public struct Flipcash_Blob_V1_InitiateExternalUploadRequest: Sendable {
/// content-length-range so storage rejects an upload that exceeds it.
public var sizeBytes: UInt64 = 0

/// Set when the bytes are end-to-end encrypted, naming the surface they are
/// encrypted for. Unset for an ordinary upload. mime_type must be
/// "application/octet-stream", or the upload is denied with
/// UNSUPPORTED_TYPE. size_bytes is the size of the whole encrypted blob and
/// is checked against UploadPolicy.encrypted.
///
/// The server cannot read the bytes, so it checks only their size. It
/// derives no metadata or renditions, does not moderate, and does not check
/// for privacy metadata. The blob can be referenced only from the surface
/// named here, and is rejected anywhere else (unencrypted MediaContent,
/// profile or chat pictures).
public var endToEndEncryptedFor: Flipcash_Blob_V1_InitiateExternalUploadRequest.OneOf_EndToEndEncryptedFor? = nil

/// The bytes are encrypted for this DM, as described in
/// messaging.v1.EncryptedContent: the 24-byte nonce followed by the
/// ciphertext and its 16-byte tag. The caller must be a member of the
/// chat and the chat must be a DM, or the upload is DENIED. Once READY,
/// the blob is granted to the chat, so the other member can read it
/// through AccessContext.chat, and it can be referenced only from
/// EncryptedContent in that chat.
public var chat: Flipcash_Common_V1_ChatId {
get {
if case .chat(let v)? = endToEndEncryptedFor {return v}
return Flipcash_Common_V1_ChatId()
}
set {endToEndEncryptedFor = .chat(newValue)}
}

public var unknownFields = SwiftProtobuf.UnknownStorage()

/// Set when the bytes are end-to-end encrypted, naming the surface they are
/// encrypted for. Unset for an ordinary upload. mime_type must be
/// "application/octet-stream", or the upload is denied with
/// UNSUPPORTED_TYPE. size_bytes is the size of the whole encrypted blob and
/// is checked against UploadPolicy.encrypted.
///
/// The server cannot read the bytes, so it checks only their size. It
/// derives no metadata or renditions, does not moderate, and does not check
/// for privacy metadata. The blob can be referenced only from the surface
/// named here, and is rejected anywhere else (unencrypted MediaContent,
/// profile or chat pictures).
public enum OneOf_EndToEndEncryptedFor: Equatable, Sendable {
/// The bytes are encrypted for this DM, as described in
/// messaging.v1.EncryptedContent: the 24-byte nonce followed by the
/// ciphertext and its 16-byte tag. The caller must be a member of the
/// chat and the chat must be a DM, or the upload is DENIED. Once READY,
/// the blob is granted to the chat, so the other member can read it
/// through AccessContext.chat, and it can be referenced only from
/// EncryptedContent in that chat.
case chat(Flipcash_Common_V1_ChatId)

}

public init() {}

fileprivate var _auth: Flipcash_Common_V1_Auth? = nil
Expand Down Expand Up @@ -526,7 +577,7 @@ extension Flipcash_Blob_V1_GetUploadPolicyResponse.Result: SwiftProtobuf._ProtoN

extension Flipcash_Blob_V1_InitiateExternalUploadRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
public static let protoMessageName: String = _protobuf_package + ".InitiateExternalUploadRequest"
public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}auth\0\u{3}mime_type\0\u{3}size_bytes\0")
public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}auth\0\u{3}mime_type\0\u{3}size_bytes\0\u{1}chat\0")

public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
Expand All @@ -537,6 +588,19 @@ extension Flipcash_Blob_V1_InitiateExternalUploadRequest: SwiftProtobuf.Message,
case 1: try { try decoder.decodeSingularMessageField(value: &self._auth) }()
case 2: try { try decoder.decodeSingularStringField(value: &self.mimeType) }()
case 3: try { try decoder.decodeSingularUInt64Field(value: &self.sizeBytes) }()
case 4: try {
var v: Flipcash_Common_V1_ChatId?
var hadOneofValue = false
if let current = self.endToEndEncryptedFor {
hadOneofValue = true
if case .chat(let m) = current {v = m}
}
try decoder.decodeSingularMessageField(value: &v)
if let v = v {
if hadOneofValue {try decoder.handleConflictingOneOf()}
self.endToEndEncryptedFor = .chat(v)
}
}()
default: break
}
}
Expand All @@ -556,13 +620,17 @@ extension Flipcash_Blob_V1_InitiateExternalUploadRequest: SwiftProtobuf.Message,
if self.sizeBytes != 0 {
try visitor.visitSingularUInt64Field(value: self.sizeBytes, fieldNumber: 3)
}
try { if case .chat(let v)? = self.endToEndEncryptedFor {
try visitor.visitSingularMessageField(value: v, fieldNumber: 4)
} }()
try unknownFields.traverse(visitor: &visitor)
}

public static func ==(lhs: Flipcash_Blob_V1_InitiateExternalUploadRequest, rhs: Flipcash_Blob_V1_InitiateExternalUploadRequest) -> Bool {
if lhs._auth != rhs._auth {return false}
if lhs.mimeType != rhs.mimeType {return false}
if lhs.sizeBytes != rhs.sizeBytes {return false}
if lhs.endToEndEncryptedFor != rhs.endToEndEncryptedFor {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
Expand Down
Loading
Loading