msgcrypto

package
v1.0.63 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 3, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

Documentation

Overview

Package msgcrypto wraps the third-party SafeChat SDK so DWS can decrypt DingTalk messages that an organization has encrypted with its own key material, and encrypt outbound ones.

The SafeChat backend links a prebuilt C static library and therefore needs CGO. Supported Darwin, Linux, and Windows amd64/arm64 builds include it by default:

CGO_ENABLED=1 go build ./cmd

Every other build gets a stub whose constructor fails with ErrUnavailable, so callers must always handle that error rather than assume the capability exists. Use Available to branch before offering the feature to a user.

Key material is fetched from the vendor key server on demand, which requires a DingTalk 免登 authCode supplied through AuthCodeProvider. DWS does not mint that code itself; the caller injects a provider.

Index

Constants

View Source
const (
	// DefaultSafeChatKeyServer is the verified SafeChat key server URL.
	DefaultSafeChatKeyServer = "https://server.safeding.com/DDSecureInter/getCorpSecureKey"
	// DefaultSafeChatRedirectHost is the host the vendor SDK reports to goProxy.
	DefaultSafeChatRedirectHost = "server.safeding.com"
)
View Source
const BackendVersion = "safechat " + safechat.Version

BackendVersion identifies the compiled-in vendor SDK.

View Source
const DefaultAuthCodeTTL = 90 * time.Second

DefaultAuthCodeTTL is the unconsumed-cache window for a freshly minted vendor authCode. The portal issues codes with expiresIn=120s and they are one-shot, so the default stays under that server window. Prefer not wrapping PortalAuthCode in CachedAuthCode: mint in goProxy and discard after the key request.

View Source
const VendorSafeChat = "safechat"

VendorSafeChat is the first vendorAuthCode vendor.

Variables

View Source
var (
	// ErrUnavailable means this binary was built without the SafeChat
	// backend, or for a platform the vendor does not ship a static library.
	ErrUnavailable = errors.New("msgcrypto: SafeChat backend not built into this binary")

	// ErrAlreadyOpen means a Cipher is already open. The underlying C
	// library keeps global state, so only one may exist per process.
	ErrAlreadyOpen = errors.New("msgcrypto: a cipher is already open in this process")

	// ErrClosed is returned by a Cipher whose Close has already run.
	ErrClosed = errors.New("msgcrypto: cipher is closed")

	// ErrNoAuthCodeProvider means Config.AuthCode was nil. Without it the
	// backend cannot fetch or rotate key material.
	ErrNoAuthCodeProvider = errors.New("msgcrypto: config.AuthCode is required")

	// ErrEmptyPayload means an encrypt or decrypt call got no bytes. The
	// vendor SDK rejects empty input, so we reject it earlier with a
	// clearer message.
	ErrEmptyPayload = errors.New("msgcrypto: payload is empty")

	// ErrNoCorpID means the caller omitted the organization id, which
	// selects the key and therefore cannot be defaulted.
	ErrNoCorpID = errors.New("msgcrypto: corpID is required")

	// ErrNoKeyServer means Config.KeyServer was empty. The vendor C
	// library would otherwise pick the key-request destination.
	ErrNoKeyServer = errors.New("msgcrypto: config.KeyServer is required")

	// ErrInvalidKeyServer means Config.KeyServer is not a usable URL.
	ErrInvalidKeyServer = errors.New("msgcrypto: config.KeyServer is not a valid URL")

	// ErrKeyServerNotHTTPS means Config.KeyServer is not https.
	ErrKeyServerNotHTTPS = errors.New("msgcrypto: config.KeyServer must be an https URL")

	// ErrRedirectHostMismatch means the domain goProxy received does not
	// match Config.AllowedRedirectHost. The domain is never sent to portal.
	ErrRedirectHostMismatch = errors.New("msgcrypto: goProxy domain host does not match AllowedRedirectHost")
)

Errors reported by this package. Callers are expected to test for ErrUnavailable explicitly, because it is the normal outcome on builds that disable CGO or target an unsupported platform.

View Source
var ErrNoAuthCode = errors.New("msgcrypto: auth code provider returned an empty code")

ErrNoAuthCode means the provider returned an empty code without an error.

Functions

func Available

func Available() bool

Available reports that this binary carries the SafeChat backend.

func DefaultKeystoreDir

func DefaultKeystoreDir() string

DefaultKeystoreDir returns the default key cache directory, ~/.dws/safechat/keystore, honouring DWS_CONFIG_DIR like the rest of DWS.

Types

type AuthCodeFunc

type AuthCodeFunc func(ctx context.Context) (string, error)

AuthCodeFunc adapts a function to AuthCodeProvider.

func (AuthCodeFunc) AuthCode

func (f AuthCodeFunc) AuthCode(ctx context.Context) (string, error)

AuthCode calls f.

type AuthCodeProvider

type AuthCodeProvider interface {
	AuthCode(ctx context.Context) (string, error)
}

AuthCodeProvider yields a DingTalk 免登 authCode for key-server authentication. DWS does not mint the code itself, so integrations inject an implementation. The backend calls this only from the vendor goProxy callback, never on every encrypt or decrypt.

Implementations must be safe for concurrent use; the backend may call this from a CGO callback while an encrypt or decrypt call is in flight.

func StaticAuthCode

func StaticAuthCode(code string) AuthCodeProvider

StaticAuthCode returns a provider that always yields code. It is meant for tests and manual integration runs; a static code stops working once the server-side five-minute window closes.

type CachedAuthCode

type CachedAuthCode struct {
	// contains filtered or unexported fields
}

CachedAuthCode memoises an AuthCodeProvider for a TTL so a burst of key requests does not trigger one upstream call each.

func NewCachedAuthCode

func NewCachedAuthCode(provider AuthCodeProvider, ttl time.Duration) *CachedAuthCode

NewCachedAuthCode wraps provider with a TTL cache. A ttl of zero or less selects DefaultAuthCodeTTL.

func (*CachedAuthCode) AuthCode

func (c *CachedAuthCode) AuthCode(ctx context.Context) (string, error)

AuthCode returns the cached code when it is still fresh, otherwise fetches a new one. A failed fetch leaves no stale value behind.

func (*CachedAuthCode) Invalidate

func (c *CachedAuthCode) Invalidate()

Invalidate drops the cached code so the next AuthCode call refetches. The backend calls this after the key server rejects a code.

type Cipher

type Cipher interface {
	// EncryptMessage encrypts plaintext for corpID/staffID and returns the
	// vendor ciphertext envelope.
	EncryptMessage(ctx context.Context, corpID, staffID string, plaintext []byte) ([]byte, error)

	// DecryptMessage decrypts a vendor ciphertext envelope.
	DecryptMessage(ctx context.Context, corpID, staffID string, ciphertext []byte) ([]byte, error)

	// Close releases the backend and frees the process-wide slot so a later
	// Open can succeed. Calling it twice is safe.
	Close() error
}

Cipher encrypts and decrypts message payloads for one organization at a time. Implementations are safe for concurrent use.

func Open

func Open(ctx context.Context, cfg Config) (Cipher, error)

Open validates cfg, prepares the keystore and starts the backend.

It returns ErrUnavailable when the backend was not compiled in, so callers can degrade gracefully. Only one Cipher may be open per process; Close frees the slot.

type Config

type Config struct {
	// KeystoreDir is where fetched keys are cached. Defaults to
	// DefaultKeystoreDir. It is created with 0700 if missing.
	KeystoreDir string

	// UserID is an opaque local identifier. The vendor SDK stores it but
	// does not use it for key selection; leave empty to let the SDK
	// generate one.
	UserID string

	// AuthCode supplies the DingTalk 免登 authCode used to authenticate key
	// requests. Required. The backend calls it only from the vendor goProxy
	// callback (cold keystore or key-version rotation), never on every
	// encrypt/decrypt.
	AuthCode AuthCodeProvider

	// KeyServer is the HTTPS URL of the vendor key service. Required: it
	// replaces the host the closed-source C library would otherwise pick.
	KeyServer string

	// AllowedRedirectHost, when set, is compared to the domain the C
	// library passes into goProxy. A mismatch fails that key fetch. It is
	// a local check only; the domain is never sent to portal.
	AllowedRedirectHost string

	// MaxRetry bounds retries while a key is still being fetched. Zero
	// selects the vendor default.
	MaxRetry int

	// HTTPTimeout bounds each key request. Zero selects the vendor default.
	HTTPTimeout time.Duration

	// Debug enables backend logging through a redacting logger. Off by
	// default because the vendor SDK logs the authCode and raw key-server
	// responses at debug level.
	Debug bool

	// Logf receives already-redacted backend log lines when Debug is set.
	// Nil discards them.
	Logf func(format string, args ...any)
}

Config parameterises Open.

type CorpAuthCodeProvider

type CorpAuthCodeProvider interface {
	AuthCodeProvider
	AuthCodeForCorp(ctx context.Context, corpID string) (string, error)
}

CorpAuthCodeProvider mints a code for a specific organization. PortalAuthCode implements this so goProxy can pass the C library's corpID. Domain and redirectURI are never part of this call.

type Identity

type Identity struct {
	CorpID  string
	StaffID string
}

Identity is the current DingTalk login organization and staff id used by message crypto. It contains no token or key material.

func CurrentIdentity

func CurrentIdentity(ctx context.Context, configDir string) (Identity, error)

CurrentIdentity reads the current login snapshot without opening SafeChat.

type PortalAuthCode

type PortalAuthCode struct {
	ConfigDir  string
	Vendor     string
	CLIVersion string
	HTTPClient *http.Client
	// contains filtered or unexported fields
}

PortalAuthCode mints a one-shot 免登 authCode from portal POST /oauth2/vendorAuthCode. It does not cache the code: goProxy spends it immediately. Do not wrap this in CachedAuthCode.

func NewPortalAuthCode

func NewPortalAuthCode(configDir, cliVersion string) *PortalAuthCode

NewPortalAuthCode returns a provider that talks to portal with the current login. cliVersion is sent as x-dws-cli-version; leave empty only when the caller cannot know the CLI version.

func (*PortalAuthCode) AuthCode

func (p *PortalAuthCode) AuthCode(ctx context.Context) (string, error)

AuthCode mints a code for the logged-in organization. Prefer AuthCodeForCorp when goProxy already has a corpID.

func (*PortalAuthCode) AuthCodeForCorp

func (p *PortalAuthCode) AuthCodeForCorp(ctx context.Context, corpID string) (string, error)

AuthCodeForCorp mints a one-shot code for corpID. The request body is only vendor + corpId.

type Session

type Session struct {
	Cipher      Cipher
	CorpID      string
	StaffID     string
	KeystoreDir string
}

Session owns one opened SafeChat cipher for the current organization.

func OpenSession

func OpenSession(ctx context.Context, opts SessionOptions) (*Session, error)

OpenSession opens a SafeChat cipher for the current login organization.

func (*Session) Close

func (s *Session) Close() error

Close releases the underlying cipher.

type SessionOptions

type SessionOptions struct {
	ConfigDir           string
	CLIVersion          string
	KeyServer           string
	AllowedRedirectHost string
	KeystoreDir         string
	Debug               bool
	Logf                func(format string, args ...any)
}

SessionOptions configures OpenSession.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL