package identity import ( "errors" "strings" "unicode" "unicode/utf8" ) // Aliases (INV-7). // // An alias is a human-readable label that an identity asserts about itself. It // is decoration for user interfaces and nothing else: // // - It is not unique. Two identities may claim the same alias, and the // protocol does nothing to prevent that. // - It is not verified. "Self-asserted" means exactly that; no one checks it. // - It never participates in signature verification or in any authorization // decision. // // Alias spoofing is therefore not a protocol vulnerability but a user // interface concern. The mitigation is presentational: never show an alias // without the address it belongs to. Display code should render // // NikoCraft (trust1q...w4np) // // and never "NikoCraft" alone. // // The validation here is not a security control. It exists to bound size and // to strip characters that let one alias impersonate another visually or // corrupt terminal output. const ( // MaxAliasLen is the maximum length of an alias in bytes. MaxAliasLen = 64 // MaxAliasRunes is the maximum length of an alias in runes, bounding the // visual width independently of UTF-8 encoding length. MaxAliasRunes = 32 ) var ( // ErrAliasTooLong is returned for an alias exceeding the size limits. ErrAliasTooLong = errors.New("identity: alias too long") // ErrAliasInvalidUTF8 is returned for an alias that is not valid UTF-8. ErrAliasInvalidUTF8 = errors.New("identity: alias is not valid UTF-8") // ErrAliasControlChar is returned for an alias containing control, // bidirectional-override or other non-printing characters. ErrAliasControlChar = errors.New("identity: alias contains a disallowed character") // ErrAliasWhitespace is returned for an alias with leading or trailing // whitespace, which would otherwise create look-alike aliases. ErrAliasWhitespace = errors.New("identity: alias has leading or trailing whitespace") ) // Alias is a validated display label. Its zero value is the empty alias, which // is always acceptable: an identity is under no obligation to name itself. type Alias struct { s string } // ParseAlias validates a self-asserted display label. // // An empty alias is valid and yields the zero Alias. func ParseAlias(s string) (Alias, error) { if s == "" { return Alias{}, nil } if len(s) > MaxAliasLen { return Alias{}, ErrAliasTooLong } if !utf8.ValidString(s) { return Alias{}, ErrAliasInvalidUTF8 } if utf8.RuneCountInString(s) > MaxAliasRunes { return Alias{}, ErrAliasTooLong } if strings.TrimSpace(s) != s { return Alias{}, ErrAliasWhitespace } for _, r := range s { if !allowedAliasRune(r) { return Alias{}, ErrAliasControlChar } } return Alias{s: s}, nil } // allowedAliasRune reports whether r may appear in an alias. // // Rejected: control characters, format characters (which include the // bidirectional overrides U+202A..U+202E and U+2066..U+2069 used to make text // render in a misleading order), unassigned code points, surrogates, private // use characters, and every space character other than a plain ASCII space. func allowedAliasRune(r rune) bool { if r == ' ' { return true } if unicode.IsControl(r) || unicode.IsSpace(r) { return false } if unicode.In(r, unicode.Cf, unicode.Cs, unicode.Co, unicode.Cn) { return false } return unicode.IsPrint(r) } // String returns the alias text. func (a Alias) String() string { return a.s } // IsEmpty reports whether the alias is unset. func (a Alias) IsEmpty() bool { return a.s == "" } // Display renders an identity for a user interface. // // The address is always included, because an alias on its own is not evidence // of anything. Callers must not build their own alias-only display strings. func Display(alias Alias, id Identity) string { addr := id.String() if alias.IsEmpty() { return addr } return alias.String() + " (" + shortAddress(addr) + ")" } // shortAddress abbreviates an address for display while keeping enough of both // ends to make substitution visible. func shortAddress(addr string) string { const head, tail = 10, 6 if len(addr) <= head+tail+1 { return addr } return addr[:head] + "\u2026" + addr[len(addr)-tail:] }