feat(server): add project sharing access control
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user