143 lines
4.1 KiB
Go
143 lines
4.1 KiB
Go
package local
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/base32"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"galaxy/user/internal/domain/common"
|
|
"galaxy/user/internal/domain/entitlement"
|
|
"galaxy/user/internal/domain/policy"
|
|
)
|
|
|
|
var base32NoPadding = base32.StdEncoding.WithPadding(base32.NoPadding)
|
|
|
|
// userNameSuffixAlphabet is the Crockford lowercase Base32 alphabet with
|
|
// `i`, `l`, `o`, and `u` excluded to avoid visual confusables. The chosen
|
|
// 32 characters also keep each byte pair aligned with a 5-bit group so the
|
|
// 5-byte random source encodes into exactly eight suffix characters.
|
|
const userNameSuffixAlphabet = "0123456789abcdefghjkmnpqrstvwxyz"
|
|
|
|
const userNameSuffixLength = 8
|
|
|
|
// IDGenerator creates opaque stable user identifiers and generated initial
|
|
// user names.
|
|
type IDGenerator struct{}
|
|
|
|
// NewUserID returns one newly generated opaque user identifier.
|
|
func (IDGenerator) NewUserID() (common.UserID, error) {
|
|
token, err := randomToken(10)
|
|
if err != nil {
|
|
return "", fmt.Errorf("generate user id: %w", err)
|
|
}
|
|
|
|
userID := common.UserID("user-" + token)
|
|
if err := userID.Validate(); err != nil {
|
|
return "", fmt.Errorf("generate user id: %w", err)
|
|
}
|
|
|
|
return userID, nil
|
|
}
|
|
|
|
// NewUserName returns one generated user name in the `player-<suffix>` form.
|
|
// The suffix is eight characters drawn from the Crockford lowercase Base32
|
|
// alphabet (confusable-free: `i`, `l`, `o`, `u` are excluded).
|
|
func (IDGenerator) NewUserName() (common.UserName, error) {
|
|
suffix, err := randomSuffix(userNameSuffixLength)
|
|
if err != nil {
|
|
return "", fmt.Errorf("generate user name: %w", err)
|
|
}
|
|
|
|
userName := common.UserName("player-" + suffix)
|
|
if err := userName.Validate(); err != nil {
|
|
return "", fmt.Errorf("generate user name: %w", err)
|
|
}
|
|
|
|
return userName, nil
|
|
}
|
|
|
|
// NewEntitlementRecordID returns one generated entitlement history record
|
|
// identifier.
|
|
func (IDGenerator) NewEntitlementRecordID() (entitlement.EntitlementRecordID, error) {
|
|
token, err := randomToken(10)
|
|
if err != nil {
|
|
return "", fmt.Errorf("generate entitlement record id: %w", err)
|
|
}
|
|
|
|
recordID := entitlement.EntitlementRecordID("entitlement-" + token)
|
|
if err := recordID.Validate(); err != nil {
|
|
return "", fmt.Errorf("generate entitlement record id: %w", err)
|
|
}
|
|
|
|
return recordID, nil
|
|
}
|
|
|
|
// NewSanctionRecordID returns one generated sanction history record
|
|
// identifier.
|
|
func (IDGenerator) NewSanctionRecordID() (policy.SanctionRecordID, error) {
|
|
token, err := randomToken(10)
|
|
if err != nil {
|
|
return "", fmt.Errorf("generate sanction record id: %w", err)
|
|
}
|
|
|
|
recordID := policy.SanctionRecordID("sanction-" + token)
|
|
if err := recordID.Validate(); err != nil {
|
|
return "", fmt.Errorf("generate sanction record id: %w", err)
|
|
}
|
|
|
|
return recordID, nil
|
|
}
|
|
|
|
// NewLimitRecordID returns one generated limit history record identifier.
|
|
func (IDGenerator) NewLimitRecordID() (policy.LimitRecordID, error) {
|
|
token, err := randomToken(10)
|
|
if err != nil {
|
|
return "", fmt.Errorf("generate limit record id: %w", err)
|
|
}
|
|
|
|
recordID := policy.LimitRecordID("limit-" + token)
|
|
if err := recordID.Validate(); err != nil {
|
|
return "", fmt.Errorf("generate limit record id: %w", err)
|
|
}
|
|
|
|
return recordID, nil
|
|
}
|
|
|
|
func randomToken(size int) (string, error) {
|
|
buffer := make([]byte, size)
|
|
if _, err := rand.Read(buffer); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return strings.ToLower(base32NoPadding.EncodeToString(buffer)), nil
|
|
}
|
|
|
|
// randomSuffix returns a length-character suffix encoded from crypto-random
|
|
// bytes through the userNameSuffixAlphabet. Each character consumes five
|
|
// random bits, so the caller receives `ceil(length * 5 / 8)` bytes of
|
|
// entropy in the underlying buffer.
|
|
func randomSuffix(length int) (string, error) {
|
|
byteCount := (length*5 + 7) / 8
|
|
buffer := make([]byte, byteCount)
|
|
if _, err := rand.Read(buffer); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
encoded := make([]byte, length)
|
|
for index := range encoded {
|
|
bitOffset := index * 5
|
|
byteIndex := bitOffset / 8
|
|
shift := bitOffset % 8
|
|
|
|
value := uint16(buffer[byteIndex]) << 8
|
|
if byteIndex+1 < len(buffer) {
|
|
value |= uint16(buffer[byteIndex+1])
|
|
}
|
|
|
|
encoded[index] = userNameSuffixAlphabet[(value>>(16-5-shift))&0x1F]
|
|
}
|
|
|
|
return string(encoded), nil
|
|
}
|