// Package securechat es un port en Go del sobre v1 "sc1" de SecureChat // (packages/crypto/src/envelope.js) y de las firmas HMAC de sdk-node/src/webhook.js. // // Si cambias algo, `go test ./...` tiene que pasar los 82 vectores de // packages/crypto/vectors/v1.json. Especificación: docs/13-INTEGRACION-OTROS-LENGUAJES.md package securechat import ( "encoding/base64" "errors" "fmt" "regexp" "unicode/utf8" ) // Códigos de error de envelope.js. Se comparan con errors.As + Code. const ( ErrInvalidInput = "INVALID_INPUT" ErrUnsupportedVersion = "UNSUPPORTED_VERSION" ErrDecryptFailed = "DECRYPT_FAILED" ErrKeyMismatch = "KEY_MISMATCH" ErrBadPadding = "BAD_PADDING" ErrSignatureInvalid = "SIGNATURE_INVALID" ErrBindingInvalid = "BINDING_INVALID" ) // CryptoError equivale a CryptoError de envelope.js: nunca se devuelve texto si hay error. type CryptoError struct { Code string Msg string } func (e *CryptoError) Error() string { return e.Code + ": " + e.Msg } func fail(code, format string, a ...any) error { return &CryptoError{Code: code, Msg: fmt.Sprintf(format, a...)} } // ErrorCode devuelve el código de un CryptoError o SignatureError ("" si es otro error). func ErrorCode(err error) string { var ce *CryptoError if errors.As(err, &ce) { return ce.Code } var se *SignatureError if errors.As(err, &se) { return se.Code } return "" } var ( b64uAlphabet = regexp.MustCompile(`^[A-Za-z0-9_-]*$`) idRe = regexp.MustCompile(`^[A-Za-z0-9_-]{1,128}$`) strictB64u = base64.RawURLEncoding.Strict() ) // B64uEncode: base64url sin relleno (RFC 4648 §5). func B64uEncode(b []byte) string { return base64.RawURLEncoding.EncodeToString(b) } // B64uDecode es estricto: solo [A-Za-z0-9_-], sin '=', sin bits sobrantes. Ojo: el // decodificador de Go ignora \r y \n incluso en modo Strict; por eso se valida el alfabeto antes. func B64uDecode(s string) ([]byte, error) { if len(s)%4 == 1 { return nil, fail(ErrInvalidInput, "longitud base64url inválida") } if !b64uAlphabet.MatchString(s) { return nil, fail(ErrInvalidInput, "carácter base64url inválido") } b, err := strictB64u.DecodeString(s) if err != nil { return nil, fail(ErrInvalidInput, "base64url inválido") } return b, nil } func decodeLen(s string, n int, what string) ([]byte, error) { b, err := B64uDecode(s) if err != nil { return nil, err } if len(b) != n { return nil, fail(ErrInvalidInput, "%s: se esperaban %d bytes, hay %d", what, n, len(b)) } return b, nil } func assertLen(b []byte, n int, what string) error { if len(b) != n { return fail(ErrInvalidInput, "%s: se esperaban %d bytes, hay %d", what, n, len(b)) } return nil } func assertID(v, what string) error { if !idRe.MatchString(v) { return fail(ErrInvalidInput, "%s debe cumplir [A-Za-z0-9_-]{1,128}", what) } return nil } func assertKeyVersion(v int) error { if v < 1 || v > 0x7fffffff { return fail(ErrInvalidInput, "keyVersion debe ser un entero entre 1 y 2^31-1") } return nil } // utf16Len cuenta como String.prototype.length de JS. func utf16Len(s string) int { n := 0 for _, r := range s { if r >= 0x10000 { n += 2 } else { n++ } } return n } // isJSBlank: lo que String.prototype.trim() de JS deja vacío. func isJSBlank(s string) bool { for _, r := range s { switch { case r >= 0x09 && r <= 0x0d, r == 0x20, r == 0xa0, r == 0x1680, r >= 0x2000 && r <= 0x200a, r == 0x2028, r == 0x2029, r == 0x202f, r == 0x205f, r == 0x3000, r == 0xfeff: continue default: return false } } return true } func validUTF8(b []byte) bool { return utf8.Valid(b) }