From c1decadb590c4d79d92bcc4df7772119a5c91244 Mon Sep 17 00:00:00 2001 From: Gab Virebent Date: Sat, 22 Aug 2026 20:36:56 +0200 Subject: Initial Aegis Usenet client release --- internal/identity/vface.go | 94 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 internal/identity/vface.go (limited to 'internal/identity/vface.go') diff --git a/internal/identity/vface.go b/internal/identity/vface.go new file mode 100644 index 0000000..a5dcef5 --- /dev/null +++ b/internal/identity/vface.go @@ -0,0 +1,94 @@ +package identity + +import ( + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "image" + "strings" + + "aegis/internal/cryptokit" +) + +const VFaceVerificationURL = "https://identicons.virebent.art" + +// VFace is the deterministic public profile derived from the three identity +// fields username, email and Ed25519 public key. The private key is never part +// of this structure. +type VFace struct { + Username string + Email string + PublicKey string + Input string + IdentityHash string + PNGHash string + FaceBase64 string + VerificationURL string +} + +// GenerateVFace creates the optional 48x48 transparent PNG used by Face and +// the profile preview. The seed is exactly username|email|public-key, matching +// the VFace implementation used by M2Usenet and N2Usenet. +func GenerateVFace(username, email, publicKey string) (VFace, error) { + username = strings.TrimSpace(username) + email = strings.TrimSpace(email) + if username == "" || email == "" { + return VFace{}, errors.New("VFace requires a username and email address") + } + if strings.ContainsAny(username+email, "\r\n|") { + return VFace{}, errors.New("VFace identity fields contain forbidden characters") + } + canonicalKey, err := cryptokit.CanonicalEd25519PublicKey(publicKey) + if err != nil { + return VFace{}, fmt.Errorf("canonicalize VFace public key: %w", err) + } + input := username + "|" + email + "|" + canonicalKey + faceBase64, err := generateFace(input, 48) + if err != nil { + return VFace{}, fmt.Errorf("generate VFace identicon: %w", err) + } + pngBytes, err := base64.StdEncoding.DecodeString(faceBase64) + if err != nil { + return VFace{}, fmt.Errorf("decode VFace PNG: %w", err) + } + if _, err := pngImage(pngBytes, 48); err != nil { + return VFace{}, err + } + identityDigest := sha256.Sum256([]byte(input)) + pngDigest := sha256.Sum256(pngBytes) + return VFace{ + Username: username, + Email: email, + PublicKey: canonicalKey, + Input: input, + IdentityHash: hex.EncodeToString(identityDigest[:]), + PNGHash: hex.EncodeToString(pngDigest[:]), + FaceBase64: faceBase64, + VerificationURL: VFaceVerificationURL, + }, nil +} + +// Headers returns the interoperable VFace headers for an article or email. +// The Face value is folded according to the existing identicon convention. +func (v VFace) Headers() []string { + return []string{ + FormatFaceHeader(v.FaceBase64), + "X-VFace-Version: 1", + "X-Ed25519-Pub: " + v.PublicKey, + "Identity-Hash: " + v.IdentityHash, + "X-VFace-Hash: sha256:" + v.IdentityHash, + "X-VFace-PNG-SHA256: " + v.PNGHash, + "X-VFace-Verify: " + v.VerificationURL, + } +} + +// DecodeFacePNG decodes a generated VFace image for a GUI preview. +func DecodeFacePNG(value string) (image.Image, error) { + data, err := base64.StdEncoding.DecodeString(strings.TrimSpace(value)) + if err != nil { + return nil, fmt.Errorf("decode Face base64: %w", err) + } + return pngImage(data, 48) +} -- cgit v1.2.3