Ctrl K
ComunidadGuías…

internal/domain/secretchat.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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
}