77 lines
3.2 KiB
Go
77 lines
3.2 KiB
Go
package nodeengineclient
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
seat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
|
"git.novox.be/novox/mesh-sdk/go/seal"
|
|
)
|
|
|
|
// The adoption handing of novox/hq ADR 0547 clause 9 (issue 1945): the node-engine of the node holding mesh-broker
|
|
// hands the bcrypt hashes of the bus accounts, in the bus module's have, sealed to the vault's own key (the seat's
|
|
// AccountsHanded), so that of every reader of the have state the vault alone can open them. These are the two ends of
|
|
// that box, which the node-engine and the vault both hold.
|
|
|
|
// HandedAccount is one user of an AccountsHanded box, opened: the bus user and the bcrypt hash the loaded list carries.
|
|
type HandedAccount struct {
|
|
User string `json:"user"`
|
|
Hash string `json:"hash"`
|
|
}
|
|
|
|
// KeyFingerprint is a public key's fingerprint as a handing names it: `sha256:<hex>` of its PEM, without the space
|
|
// around it.
|
|
func KeyFingerprint(publicPEM string) string {
|
|
sum := sha256.Sum256([]byte(strings.TrimSpace(publicPEM)))
|
|
return "sha256:" + hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
// bcryptHash is the form of a bcrypt hash: version, cost, and 53 characters of salt and digest.
|
|
var bcryptHash = regexp.MustCompile(`^\$2[abxy]?\$[0-9]{2}\$[./A-Za-z0-9]{53}$`)
|
|
|
|
// IsBcrypt says whether s has the form of a bcrypt hash.
|
|
func IsBcrypt(s string) bool { return bcryptHash.MatchString(s) }
|
|
|
|
// HandAccounts seals the accounts to the vault's public key (SPKI PEM) as the handing of the list whose digest is from.
|
|
func HandAccounts(accounts []HandedAccount, vaultPEM, from string, at time.Time) (seat.AccountsHanded, error) {
|
|
if strings.TrimSpace(vaultPEM) == "" {
|
|
return seat.AccountsHanded{}, errors.New("node-engine: no vault key to seal the handed accounts to")
|
|
}
|
|
body, err := json.Marshal(accounts)
|
|
if err != nil {
|
|
return seat.AccountsHanded{}, err
|
|
}
|
|
box, err := seal.Seal(string(body), vaultPEM)
|
|
if err != nil {
|
|
return seat.AccountsHanded{}, fmt.Errorf("node-engine: the handed accounts could not be sealed to the vault's key: %w", err)
|
|
}
|
|
return seat.AccountsHanded{From: from, To: KeyFingerprint(vaultPEM), Users: int64(len(accounts)),
|
|
Box: seat.SealedBox{Ct: box.Ct, Eph: box.Eph, Iv: box.IV, Tag: box.Tag, V: int64(box.V)}, At: at.UTC()}, nil
|
|
}
|
|
|
|
// OpenHanded opens a handing with the vault's private key (PEM), refusing one sealed to another key than publicPEM's
|
|
// and a box that does not hold as many users as it says.
|
|
func OpenHanded(h seat.AccountsHanded, publicPEM, privatePEM string) ([]HandedAccount, error) {
|
|
if h.To != KeyFingerprint(publicPEM) {
|
|
return nil, fmt.Errorf("node-engine: the handing is sealed to the key %s, not to this one (%s)", h.To, KeyFingerprint(publicPEM))
|
|
}
|
|
plain, err := seal.Open(seal.Box{V: int(h.Box.V), Eph: h.Box.Eph, IV: h.Box.Iv, Tag: h.Box.Tag, Ct: h.Box.Ct}, privatePEM)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("node-engine: the handing does not open: %w", err)
|
|
}
|
|
var out []HandedAccount
|
|
if err := json.Unmarshal([]byte(plain), &out); err != nil {
|
|
return nil, fmt.Errorf("node-engine: the handing holds no accounts: %w", err)
|
|
}
|
|
if int64(len(out)) != h.Users {
|
|
return nil, fmt.Errorf("node-engine: the handing says %d users and holds %d", h.Users, len(out))
|
|
}
|
|
return out, nil
|
|
}
|