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:` 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 }