initial: ZeroTier-like P2P mesh VPN server with multi-tenant Web UI
This commit is contained in:
142
internal/identity/identity.go
Normal file
142
internal/identity/identity.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package identity
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Identity struct {
|
||||
PublicKey ed25519.PublicKey `json:"public_key"`
|
||||
PrivateKey ed25519.PrivateKey `json:"private_key"`
|
||||
Address Address `json:"address"`
|
||||
}
|
||||
|
||||
type Address [5]byte
|
||||
|
||||
func (a Address) String() string {
|
||||
return hex.EncodeToString(a[:])
|
||||
}
|
||||
|
||||
func (a Address) MarshalText() ([]byte, error) {
|
||||
return []byte(a.String()), nil
|
||||
}
|
||||
|
||||
func (a *Address) UnmarshalText(text []byte) error {
|
||||
decoded, err := hex.DecodeString(string(text))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(decoded) != 5 {
|
||||
return fmt.Errorf("address must be 5 bytes")
|
||||
}
|
||||
copy(a[:], decoded)
|
||||
return nil
|
||||
}
|
||||
|
||||
func AddressFromPublicKey(pubKey []byte) Address {
|
||||
var addr Address
|
||||
h := hashBytes(pubKey)
|
||||
copy(addr[:], h[:5])
|
||||
return addr
|
||||
}
|
||||
|
||||
func Generate() *Identity {
|
||||
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &Identity{
|
||||
PublicKey: pub,
|
||||
PrivateKey: priv,
|
||||
Address: AddressFromPublicKey(pub),
|
||||
}
|
||||
}
|
||||
|
||||
func (id *Identity) PublicKeyHex() string {
|
||||
return hex.EncodeToString(id.PublicKey)
|
||||
}
|
||||
|
||||
func (id *Identity) PrivateKeyHex() string {
|
||||
return hex.EncodeToString(id.PrivateKey)
|
||||
}
|
||||
|
||||
func (id *Identity) Sign(data []byte) []byte {
|
||||
return ed25519.Sign(id.PrivateKey, data)
|
||||
}
|
||||
|
||||
func (id *Identity) Verify(data, sig []byte) bool {
|
||||
return ed25519.Verify(id.PublicKey, data, sig)
|
||||
}
|
||||
|
||||
func LoadOrGenerate(path string) (*Identity, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err == nil {
|
||||
return Parse(strings.TrimSpace(string(data)))
|
||||
}
|
||||
if !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
id := Generate()
|
||||
encoded, err := id.Serialize()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := os.MkdirAll(dir(path), 0755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(encoded), 0600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (id *Identity) Serialize() (string, error) {
|
||||
data, err := json.Marshal(id)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func Parse(data string) (*Identity, error) {
|
||||
var id Identity
|
||||
if err := json.Unmarshal([]byte(data), &id); err != nil {
|
||||
return nil, fmt.Errorf("parse identity: %w", err)
|
||||
}
|
||||
if len(id.PrivateKey) == 0 {
|
||||
return nil, fmt.Errorf("invalid identity: no private key")
|
||||
}
|
||||
id.Address = AddressFromPublicKey(id.PublicKey)
|
||||
return &id, nil
|
||||
}
|
||||
|
||||
func dir(path string) string {
|
||||
idx := strings.LastIndex(path, "/")
|
||||
if idx == -1 {
|
||||
idx = strings.LastIndex(path, "\\")
|
||||
}
|
||||
if idx == -1 {
|
||||
return "."
|
||||
}
|
||||
return path[:idx]
|
||||
}
|
||||
|
||||
func hashBytes(data []byte) []byte {
|
||||
h := make([]byte, 32)
|
||||
for i, b := range data {
|
||||
h[i%32] ^= b
|
||||
}
|
||||
// simple hash expansion
|
||||
for round := 0; round < 3; round++ {
|
||||
for i := 0; i < 32; i++ {
|
||||
h[i] = h[i] ^ h[(i+1)%32] ^ h[(i+7)%32]
|
||||
h[i] = (h[i] << 3) | (h[i] >> 5)
|
||||
}
|
||||
}
|
||||
return h
|
||||
}
|
||||
Reference in New Issue
Block a user