package sharing import ( "crypto/aes" "crypto/cipher" "crypto/rand" "crypto/sha256" "crypto/subtle" "encoding/hex" "fmt" "io" "net/mail" "regexp" "strings" ) const shareIDLength = 27 var ( base62Alphabet = []byte("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") shareIDPattern = regexp.MustCompile(`^[0-9A-Za-z]{27}$`) phonePattern = regexp.MustCompile(`^\+?[0-9]{6,20}$`) ) type secureCodec struct { aead cipher.AEAD rand io.Reader } func newSecureCodec(secret string) (*secureCodec, error) { secret = strings.TrimSpace(secret) if secret == "" { return nil, fmt.Errorf("%w: sharing encryption secret is required", ErrInvalidInput) } key := sha256.Sum256([]byte(secret)) block, err := aes.NewCipher(key[:]) if err != nil { return nil, err } aead, err := cipher.NewGCM(block) if err != nil { return nil, err } return &secureCodec{aead: aead, rand: rand.Reader}, nil } func (c *secureCodec) newShareID() (string, error) { result := make([]byte, 0, shareIDLength) buffer := make([]byte, 64) for len(result) < shareIDLength { if _, err := io.ReadFull(c.rand, buffer); err != nil { return "", err } for _, value := range buffer { if value >= 248 { continue } result = append(result, base62Alphabet[int(value)%len(base62Alphabet)]) if len(result) == shareIDLength { break } } } return string(result), nil } func (c *secureCodec) encrypt(value string) ([]byte, error) { nonce := make([]byte, c.aead.NonceSize()) if _, err := io.ReadFull(c.rand, nonce); err != nil { return nil, err } return c.aead.Seal(nonce, nonce, []byte(value), nil), nil } func (c *secureCodec) decrypt(value []byte) (string, error) { if len(value) < c.aead.NonceSize() { return "", fmt.Errorf("%w: encrypted share value is invalid", ErrInvalidInput) } nonce := value[:c.aead.NonceSize()] plaintext, err := c.aead.Open(nil, nonce, value[c.aead.NonceSize():], nil) if err != nil { return "", err } return string(plaintext), nil } func hashValue(value string) []byte { digest := sha256.Sum256([]byte(value)) return digest[:] } func hashID(value []byte) string { if len(value) > 16 { value = value[:16] } return hex.EncodeToString(value) } func hashesEqual(left []byte, right []byte) bool { return len(left) == len(right) && subtle.ConstantTimeCompare(left, right) == 1 } func validShareID(value string) bool { return shareIDPattern.MatchString(value) } func normalizeIdentifier(value string) (string, string, error) { value = strings.TrimSpace(value) if value == "" { return "", "", fmt.Errorf("%w: email or phone is required", ErrInvalidInput) } if strings.Contains(value, "@") { address, err := mail.ParseAddress(value) if err != nil || !strings.EqualFold(strings.TrimSpace(address.Address), value) { return "", "", fmt.Errorf("%w: email is invalid", ErrInvalidInput) } return "email", strings.ToLower(address.Address), nil } phone := strings.NewReplacer(" ", "", "-", "", "(", "", ")", "").Replace(value) if !phonePattern.MatchString(phone) { return "", "", fmt.Errorf("%w: phone is invalid", ErrInvalidInput) } return "phone", phone, nil } func actorIdentifiers(actor Actor) []string { seen := make(map[string]struct{}) appendIdentifier := func(values *[]string, value string) { kind, normalized, err := normalizeIdentifier(value) if err != nil || kind == "" { return } key := kind + ":" + normalized if _, ok := seen[key]; ok { return } seen[key] = struct{}{} *values = append(*values, key) } values := make([]string, 0, 3) appendIdentifier(&values, actor.Email) appendIdentifier(&values, actor.Phone) if strings.TrimSpace(actor.CountryCode) != "" && strings.TrimSpace(actor.Phone) != "" { countryCode := strings.TrimPrefix(strings.TrimSpace(actor.CountryCode), "+") phone := strings.TrimPrefix(strings.TrimSpace(actor.Phone), "+") appendIdentifier(&values, "+"+countryCode+phone) } return values } func ActorOwnsIdentifier(actor Actor, identifier string) bool { identifierType, normalized, err := normalizeIdentifier(identifier) if err != nil { return false } candidateHash := hashValue(identifierType + ":" + normalized) for _, actorIdentifier := range actorIdentifiers(actor) { if hashesEqual(candidateHash, hashValue(actorIdentifier)) { return true } } return false }