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) }