mesh/delivery merged: source.main moved to novox/mesh-catalog@6d000647de2d
mesh/check being carried to cff1005422c1 on 6d000647de2d | from f9dcbc295723
The one bundle that did everything becomes the manager bundle, which runs long and answers the seat's ten verbs and the three acting tools, and a tools bundle of its own that answers the four reading tools and writes nothing, so they answer while the manager fails to load or stops. Both import the module's one package for the layouts of the kept data and the log entries, the key form, the entries' names and the settings with their validity; the settings file and the account with its directory stay as they are. Builds: design 39 parts §1, §2, §3, §4, §5, §6, §7, §8
190 lines
5.2 KiB
Go
190 lines
5.2 KiB
Go
package main
|
|
|
|
// Sealing to one recipient (novox/hq ADR 0183, ADR 0206): the manager seals what it hands a consumer to
|
|
// the key that consumer sent, and a node seals a waiting login to the key the manager gives. The same box
|
|
// the agent module's TypeScript makes and opens, byte for byte — X25519 for the agreement, HKDF-SHA256 for
|
|
// the key, AES-256-GCM for the box — so `testdata/sealed-by-typescript.json` is opened here, and a test
|
|
// reopens what this seals with the same derivation.
|
|
//
|
|
// A box is `{ v: 1, eph, iv, tag, ct }`, every field base64; `eph` is the one-time public key as SPKI DER,
|
|
// and the key is bound to it and to the recipient's raw public key, so a box cannot be re-addressed.
|
|
|
|
import (
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/ecdh"
|
|
"crypto/hkdf"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
)
|
|
|
|
// SealedBox is a value sealed to one recipient.
|
|
type SealedBox struct {
|
|
V int `json:"v"`
|
|
Eph string `json:"eph"`
|
|
IV string `json:"iv"`
|
|
Tag string `json:"tag"`
|
|
Ct string `json:"ct"`
|
|
}
|
|
|
|
// KeyPair is a recipient's keypair as the two PEM strings it is kept and sent as.
|
|
type KeyPair struct {
|
|
PublicKey string `json:"publicKey"`
|
|
PrivateKey string `json:"privateKey"`
|
|
}
|
|
|
|
const sealInfo = "novox-mesh sealed box v1"
|
|
|
|
// GenerateKeyPair makes an X25519 keypair, PEM-encoded as the agent module's are.
|
|
func GenerateKeyPair() (KeyPair, error) {
|
|
priv, err := ecdh.X25519().GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
return KeyPair{}, err
|
|
}
|
|
pubDER, err := x509.MarshalPKIXPublicKey(priv.PublicKey())
|
|
if err != nil {
|
|
return KeyPair{}, err
|
|
}
|
|
privDER, err := x509.MarshalPKCS8PrivateKey(priv)
|
|
if err != nil {
|
|
return KeyPair{}, err
|
|
}
|
|
return KeyPair{
|
|
PublicKey: string(pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubDER})),
|
|
PrivateKey: string(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privDER})),
|
|
}, nil
|
|
}
|
|
|
|
func publicFromPEM(p string) (*ecdh.PublicKey, error) {
|
|
block, _ := pem.Decode([]byte(p))
|
|
if block == nil {
|
|
return nil, errors.New("not a PEM public key")
|
|
}
|
|
k, err := x509.ParsePKIXPublicKey(block.Bytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pub, ok := k.(*ecdh.PublicKey)
|
|
if !ok || pub.Curve() != ecdh.X25519() {
|
|
return nil, errors.New("not an X25519 public key")
|
|
}
|
|
return pub, nil
|
|
}
|
|
|
|
func privateFromPEM(p string) (*ecdh.PrivateKey, error) {
|
|
block, _ := pem.Decode([]byte(p))
|
|
if block == nil {
|
|
return nil, errors.New("not a PEM private key")
|
|
}
|
|
k, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
priv, ok := k.(*ecdh.PrivateKey)
|
|
if !ok || priv.Curve() != ecdh.X25519() {
|
|
return nil, errors.New("not an X25519 private key")
|
|
}
|
|
return priv, nil
|
|
}
|
|
|
|
func boxKey(secret, ephDER, recipientRaw []byte) ([]byte, error) {
|
|
salt := append(append([]byte{}, ephDER...), recipientRaw...)
|
|
return hkdf.Key(sha256.New, secret, salt, sealInfo, 32)
|
|
}
|
|
|
|
// Seal seals plaintext to the recipient's public key.
|
|
func Seal(plaintext, recipientPEM string) (SealedBox, error) {
|
|
recipient, err := publicFromPEM(recipientPEM)
|
|
if err != nil {
|
|
return SealedBox{}, err
|
|
}
|
|
eph, err := ecdh.X25519().GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
return SealedBox{}, err
|
|
}
|
|
secret, err := eph.ECDH(recipient)
|
|
if err != nil {
|
|
return SealedBox{}, err
|
|
}
|
|
ephDER, err := x509.MarshalPKIXPublicKey(eph.PublicKey())
|
|
if err != nil {
|
|
return SealedBox{}, err
|
|
}
|
|
key, err := boxKey(secret, ephDER, recipient.Bytes())
|
|
if err != nil {
|
|
return SealedBox{}, err
|
|
}
|
|
gcm, err := newGCM(key)
|
|
if err != nil {
|
|
return SealedBox{}, err
|
|
}
|
|
iv := make([]byte, 12)
|
|
if _, err := rand.Read(iv); err != nil {
|
|
return SealedBox{}, err
|
|
}
|
|
out := gcm.Seal(nil, iv, []byte(plaintext), nil)
|
|
ct, tag := out[:len(out)-gcm.Overhead()], out[len(out)-gcm.Overhead():]
|
|
b64 := base64.StdEncoding.EncodeToString
|
|
return SealedBox{V: 1, Eph: b64(ephDER), IV: b64(iv), Tag: b64(tag), Ct: b64(ct)}, nil
|
|
}
|
|
|
|
// Open opens a box with the recipient's private key; it fails for a box to another key or one tampered with.
|
|
func Open(box SealedBox, privatePEM string) (string, error) {
|
|
if box.V != 1 {
|
|
return "", errors.New("not a sealed box this module can open")
|
|
}
|
|
priv, err := privateFromPEM(privatePEM)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
d := base64.StdEncoding.DecodeString
|
|
ephDER, err := d(box.Eph)
|
|
if err != nil {
|
|
return "", fmt.Errorf("the box's eph: %w", err)
|
|
}
|
|
ephKey, err := x509.ParsePKIXPublicKey(ephDER)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
eph, ok := ephKey.(*ecdh.PublicKey)
|
|
if !ok {
|
|
return "", errors.New("the box's eph is not an X25519 key")
|
|
}
|
|
secret, err := priv.ECDH(eph)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
key, err := boxKey(secret, ephDER, priv.PublicKey().Bytes())
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
iv, err1 := d(box.IV)
|
|
tag, err2 := d(box.Tag)
|
|
ct, err3 := d(box.Ct)
|
|
if err := errors.Join(err1, err2, err3); err != nil {
|
|
return "", err
|
|
}
|
|
gcm, err := newGCM(key)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
plain, err := gcm.Open(nil, iv, append(ct, tag...), nil)
|
|
if err != nil {
|
|
return "", errors.New("the box does not open with this key")
|
|
}
|
|
return string(plain), nil
|
|
}
|
|
|
|
func newGCM(key []byte) (cipher.AEAD, error) {
|
|
block, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return cipher.NewGCM(block)
|
|
}
|