internal/domain/secretchat.go
Rama main · réplica de lectura del repositorio original.
package domain
import "errors"
// SecretChatState is the server-authoritative state of the private end-to-end encrypted (Secret Chat / EncryptedChat) handshake state machine.
// The server is a blind relay: g_a/g_b/key_fingerprint/encrypted bytes are all stored opaquely
// and forwarded verbatim, never touching the DH shared secret or plaintext. Design in docs/secret-chat-module.md.
type SecretChatState string
const (
// SecretChatStateRequested: requestEncryption has been accepted and is waiting for the recipient's acceptEncryption.
// The same server state projects as encryptedChatWaiting for the initiator and encryptedChatRequested for the recipient.
SecretChatStateRequested SecretChatState = "requested"
// SecretChatStateNormal: acceptEncryption has persisted g_b/key_fingerprint and the handshake is established.
SecretChatStateNormal SecretChatState = "normal"
// SecretChatStateDiscarded: terminal state, triggered by either side's discardEncryption.
SecretChatStateDiscarded SecretChatState = "discarded"
)
// Secret chat storage-layer errors (shared by the memory/postgres implementations, with the behavior contract pinned by storetest).
var (
// ErrSecretChatNotFound: chat_id does not exist → CHAT_ID_INVALID / ENCRYPTION_ID_INVALID.
ErrSecretChatNotFound = errors.New("secretchat: not found")
// ErrSecretChatAlreadyAccepted: accepting an already-established secret chat → ENCRYPTION_ALREADY_ACCEPTED.
ErrSecretChatAlreadyAccepted = errors.New("secretchat: already accepted")
// ErrSecretChatAlreadyDeclined: accepting a discarded secret chat → ENCRYPTION_ALREADY_DECLINED.
ErrSecretChatAlreadyDeclined = errors.New("secretchat: already declined")
// ErrSecretChatRandomIDDuplicate: requestEncryption.random_id is already taken by a different intent or another
// auth key. random_id is itself the chat_id, so allocating another ID to dodge a collision is forbidden.
ErrSecretChatRandomIDDuplicate = errors.New("secretchat: random id duplicate")
)
// SecretChat is the server-authoritative state of one private secret chat (durable, surviving restarts).
// Field names align with TL encryptedChat*; ID is int32-scale and access_hash/admin_id/
// participant_id/key_fingerprint are int64. The binding dimension is device-level (the perm auth_key's int64 value).
type SecretChat struct {
// ID is the chat_id and must equal requestEncryption.random_id bit for bit (non-zero int32); both sides share the same id.
ID int
// AdminAccessHash / ParticipantAccessHash differ between the two perspectives (TL "check sum depending on user ID").
AdminAccessHash int64
ParticipantAccessHash int64
// AdminUserID is the initiator and ParticipantUserID the recipient; roles are determined from these.
AdminUserID int64
ParticipantUserID int64
// AdminAuthKeyID / ParticipantAuthKeyID are the bound device's perm auth_key (the little-endian authKeyIDToInt64 value).
// ParticipantAuthKeyID is 0 before accept (before the link is established the invite is visible to the participant at account level).
AdminAuthKeyID int64
ParticipantAuthKeyID int64
State SecretChatState
// GA is the initiator's public key carried by requestEncryption (blindly stored, left-padded with zeros to 256B).
// GB is the recipient's public key carried by acceptEncryption (persisted after accept).
// Projection GAOrB: it is GB for the admin's perspective and GA for the participant's (pinned by the TL comments).
GA []byte
GB []byte
// KeyFingerprint is the shared-key fingerprint computed by the recipient; the server relays the int64 verbatim and never recomputes it.
KeyFingerprint int64
// Layer is the secret chat's inner layer snapshot (not parsed); FolderID passes through the requested archive intent.
Layer int
FolderID int
// HistoryDeleted is whether discard asked the peer to delete the entire chat history.
HistoryDeleted bool
// RandomID is requestEncryption's int32 idempotency key (the same initiating device with the same random_id returns the same chat).
RandomID int32
// Date is the acceptance time (unix seconds).
Date int
}
// Terminal reports whether the secret chat has been discarded.
func (c SecretChat) Terminal() bool {
return c.State == SecretChatStateDiscarded
}
// HasParticipant reports whether userID is the initiator or recipient of this secret chat.
func (c SecretChat) HasParticipant(userID int64) bool {
return userID != 0 && (userID == c.AdminUserID || userID == c.ParticipantUserID)
}
// IsAdmin reports whether userID is the initiator.
func (c SecretChat) IsAdmin(userID int64) bool {
return userID != 0 && userID == c.AdminUserID
}
// PeerOf returns userID's peer in this secret chat; returns 0 when userID is not a participant.
func (c SecretChat) PeerOf(userID int64) int64 {
switch userID {
case c.AdminUserID:
return c.ParticipantUserID
case c.ParticipantUserID:
return c.AdminUserID
default:
return 0
}
}
// PeerAuthKeyOf returns the peer's bound device perm auth_key (int64) for userID; returns 0 when userID is not a participant
// or the peer is unbound. The admin's peer is the participant device and vice versa.
func (c SecretChat) PeerAuthKeyOf(userID int64) int64 {
switch userID {
case c.AdminUserID:
return c.ParticipantAuthKeyID
case c.ParticipantUserID:
return c.AdminAuthKeyID
default:
return 0
}
}
// AuthKeyOf returns the permanent auth key bound to userID itself; returns 0 when userID is not a participant or not yet bound.
// Every read/write authorization for an established secret chat must match both the user and this auth key, never relying on account identity alone.
func (c SecretChat) AuthKeyOf(userID int64) int64 {
switch userID {
case c.AdminUserID:
return c.AdminAuthKeyID
case c.ParticipantUserID:
return c.ParticipantAuthKeyID
default:
return 0
}
}
// AccessHashFor returns the access_hash from userID's perspective (different for the two sides); returns 0 for a non-participant.
func (c SecretChat) AccessHashFor(userID int64) int64 {
switch userID {
case c.AdminUserID:
return c.AdminAccessHash
case c.ParticipantUserID:
return c.ParticipantAccessHash
default:
return 0
}
}
// EncryptedFileRef is the server snapshot of a secret chat file (P2, blind relay: the content is encrypted bytes and is not decrypted).
type EncryptedFileRef struct {
ID int64
AccessHash int64
Size int64
DCID int
KeyFingerprint int // int32 scale
}
// SecretChatMessage is one opaque encrypted message in the secret chat qts delivery queue (the payload of
// updateNewEncryptedMessage). Bytes is the client-encrypted DecryptedMessage, blindly stored and relayed by the server and never decrypted.
// Delivered on the qts sequence of the receiving device (ReceiverAuthKeyID).
type SecretChatMessage struct {
ReceiverAuthKeyID int64
Qts int
ReceiverUserID int64
ChatID int
RandomID int64
Date int
IsService bool
Bytes []byte
File *EncryptedFileRef
}
// EncryptedStateEventType distinguishes the kinds of qts-less secret chat state events.
type EncryptedStateEventType int
const (
// EncryptedStateEventEncryption: updateEncryption (handshake state change); rebuilt from the secret_chats
// authoritative state when delivered (no snapshot is frozen).
EncryptedStateEventEncryption EncryptedStateEventType = 1
// EncryptedStateEventRead: updateEncryptedMessagesRead (a read receipt), carrying MaxDate.
EncryptedStateEventRead EncryptedStateEventType = 2
)
// EncryptedStateEvent is a qts-less durable secret chat state event (for offline getDifference catch-up).
// TargetAuthKeyID=0 means account-level (visible to all devices); non-zero means targeted at a bound device.
type EncryptedStateEvent struct {
ID int64
TargetUserID int64
TargetAuthKeyID int64
ChatID int
Type EncryptedStateEventType
MaxDate int
Date int
}
// SecretMessageDelivery is one encrypted message sendEncrypted* is to deliver to the peer device (assembled by the rpc layer,
// with the service assigning the receiving device's qts and persisting it).
type SecretMessageDelivery struct {
RandomID int64
Bytes []byte
IsService bool
File *EncryptedFileRef
Date int
}
// MaxSecretMessageDataBytes bounds the opaque encrypted DecryptedMessage payload persisted in
// the device queue. Secret-chat media bytes travel through the file service, so a 1 MiB metadata
// envelope is already well above the payload emitted by the supported clients while keeping one
// request independent from the process-wide MTProto admission budget.
const MaxSecretMessageDataBytes = 1 << 20
// SecretChatRequest is requestEncryption's acceptance input (privacy/block/self/bot checks run first in the rpc layer).
type SecretChatRequest struct {
AdminUserID int64
AdminAuthKeyID int64
ParticipantUserID int64
RandomID int32
GA []byte
Date int
}