summaryrefslogtreecommitdiffstats
path: root/internal/identity/vface.go
blob: a5dcef5f41cc1f2c92767ae329a1146ecc770854 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
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)
}