mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-08 22:49:10 +02:00
Merge branch 'main' into poc/certificate-posture
This commit is contained in:
@@ -285,6 +285,25 @@ func (u *User) EncryptSensitiveData(enc *crypt.FieldEncrypt) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func MaskEmail(email string) string {
|
||||
local, domain, found := strings.Cut(email, "@")
|
||||
if !found || local == "" || domain == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Runes, not bytes, so a non-ASCII local part is not cut mid-character.
|
||||
runes := []rune(local)
|
||||
|
||||
// Keeping the first two and the last needs a local part of at least four to
|
||||
// hide anything at all: at three or fewer those are the whole of it, and the
|
||||
// address would be recoverable in full from what is meant to conceal it.
|
||||
if len(runes) < 4 {
|
||||
return "****@" + domain
|
||||
}
|
||||
|
||||
return string(runes[:2]) + "****" + string(runes[len(runes)-1]) + "@" + domain
|
||||
}
|
||||
|
||||
// DecryptSensitiveData decrypts the user's sensitive fields (Email and Name) in place.
|
||||
func (u *User) DecryptSensitiveData(enc *crypt.FieldEncrypt) error {
|
||||
if enc == nil {
|
||||
|
||||
@@ -296,3 +296,144 @@ func TestUser_EncryptDecryptRoundTrip(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaskEmail(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
email string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "ordinary address keeps the first two, the last, and the domain",
|
||||
email: "admin@example.com",
|
||||
expected: "ad****n@example.com",
|
||||
},
|
||||
{
|
||||
name: "four characters is the shortest local part that reveals anything",
|
||||
email: "abcd@example.com",
|
||||
expected: "ab****d@example.com",
|
||||
},
|
||||
{
|
||||
name: "three character local part is masked whole, since a lead and tail would be all of it",
|
||||
email: "abc@example.com",
|
||||
expected: "****@example.com",
|
||||
},
|
||||
{
|
||||
name: "two character local part is masked whole",
|
||||
email: "ab@example.com",
|
||||
expected: "****@example.com",
|
||||
},
|
||||
{
|
||||
name: "single character local part is masked whole",
|
||||
email: "a@b.co",
|
||||
expected: "****@b.co",
|
||||
},
|
||||
{
|
||||
name: "mask width does not report the length it stands in for",
|
||||
email: "a.very.long.local.part@example.com",
|
||||
expected: "a.****t@example.com",
|
||||
},
|
||||
{
|
||||
name: "a local part far longer than the mask is still reduced to three characters",
|
||||
email: "finance.department.notifications.owner.account@example.com",
|
||||
expected: "fi****t@example.com",
|
||||
},
|
||||
{
|
||||
name: "plus addressing is masked along with the rest of the local part",
|
||||
email: "admin+netbird@example.com",
|
||||
expected: "ad****d@example.com",
|
||||
},
|
||||
{
|
||||
name: "separators inside the local part are not treated specially",
|
||||
email: "first.last-name_x@example.com",
|
||||
expected: "fi****x@example.com",
|
||||
},
|
||||
{
|
||||
name: "case is preserved rather than normalised",
|
||||
email: "Admin@Example.COM",
|
||||
expected: "Ad****n@Example.COM",
|
||||
},
|
||||
{
|
||||
name: "subdomains stay intact",
|
||||
email: "owner@mail.corp.example.com",
|
||||
expected: "ow****r@mail.corp.example.com",
|
||||
},
|
||||
{
|
||||
name: "german umlauts count as single characters",
|
||||
email: "müller@example.de",
|
||||
expected: "mü****r@example.de",
|
||||
},
|
||||
{
|
||||
name: "cyrillic local part is cut on runes",
|
||||
email: "иванов@example.ru",
|
||||
expected: "ив****в@example.ru",
|
||||
},
|
||||
{
|
||||
name: "cjk local part of three runes is masked whole, counted in runes not bytes",
|
||||
email: "用户名@example.cn",
|
||||
expected: "****@example.cn",
|
||||
},
|
||||
{
|
||||
name: "cjk local part of four runes reveals the first two and the last",
|
||||
email: "用户名字@example.cn",
|
||||
expected: "用户****字@example.cn",
|
||||
},
|
||||
{
|
||||
name: "arabic local part is cut on runes",
|
||||
email: "مستخدم@example.sa",
|
||||
expected: "مس****م@example.sa",
|
||||
},
|
||||
{
|
||||
name: "two rune non-ascii local part is masked whole",
|
||||
email: "ää@example.de",
|
||||
expected: "****@example.de",
|
||||
},
|
||||
{
|
||||
name: "astral plane runes are not split into surrogates",
|
||||
email: "a🎉bc@example.com",
|
||||
expected: "a🎉****c@example.com",
|
||||
},
|
||||
{
|
||||
name: "a non-ascii domain is left alone",
|
||||
email: "admin@münchen.example",
|
||||
expected: "ad****n@münchen.example",
|
||||
},
|
||||
{
|
||||
name: "only the first separator splits, so a second stays in the domain",
|
||||
email: "a@b@example.com",
|
||||
expected: "****@b@example.com",
|
||||
},
|
||||
{
|
||||
name: "empty email has nothing to mask",
|
||||
email: "",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "value without a separator is not an address",
|
||||
email: "not-an-email",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "missing local part is not an address",
|
||||
email: "@example.com",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "missing domain is not an address",
|
||||
email: "admin@",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "a bare separator is not an address",
|
||||
email: "@",
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert.Equal(t, tc.expected, MaskEmail(tc.email))
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user