package delivery import ( "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "regexp" "strings" "time" "git.novox.be/novox/mesh-sdk/go/seal" ) // The adoption handing of novox/hq ADR 0547 clause 9 (issue 1945): the vault adopts, once and before `take-over`, the // bcrypt hashes of the bus accounts it holds no value for (the controller's own, the node accounts, the enrolled // devices) from the list the bus has loaded. The node-engine of the node holding mesh-broker is the one part that has // that list, so it hands the hashes in the bus module's have, beside what it says of the list it wrote: sealed to the // vault's own key, which it learns from the vault's answer to `node-key`, so that of every reader of the have bucket // the vault alone can open them. The have says in the clear only how many users it hands, from which list and to which // key; the users' names stay sealed, as the list itself is (ADR 0547 clause 7: it names every account). // // It hands every user of the loaded list; the vault takes from it only the users it holds no value for, and never // replaces a hash it made from a value it holds, so the node-engine never needs to read the vault's state (which the // grants part alone is to read, clause 9). // AccountsHanded is the hashes of the loaded list's users, sealed to the vault, in UserListWritten. type AccountsHanded struct { // From is the digest of the list on the machine the hashes are taken from, `sha256:` (UserListWritten.Digest). From string `json:"from"` // To is the fingerprint of the vault's key the box is sealed to (KeyFingerprint). To string `json:"to"` // Users is how many users the box holds. Users int `json:"users"` // Box is the JSON of []HandedAccount, sealed to the vault's key (ADR 0206). Box seal.Box `json:"box"` At time.Time `json:"at"` } // HandedAccount is one user of an AccountsHanded box, opened: the bus user and the bcrypt hash the loaded list // carries for it. 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[:]) } // listedAccount is one user of a bus user list as the composer writes it (buscompose), with its password's hash. var listedAccount = regexp.MustCompile(`\{\s*user:\s*"([^"]+)",\s*password:\s*"([^"]+)"`) // HandedAccountsOf is every user of a bus user list that carries a bcrypt hash, in the list's order. A user listed // twice is handed once, as the list first names it. func HandedAccountsOf(list []byte) []HandedAccount { var out []HandedAccount seen := map[string]bool{} for _, m := range listedAccount.FindAllSubmatch(list, -1) { user, hash := string(m[1]), string(m[2]) if seen[user] || !IsBcrypt(hash) { continue } seen[user] = true out = append(out, HandedAccount{User: user, Hash: hash}) } return out } // 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) (AccountsHanded, error) { if strings.TrimSpace(vaultPEM) == "" { return AccountsHanded{}, errors.New("delivery: no vault key to seal the handed accounts to") } body, err := json.Marshal(accounts) if err != nil { return AccountsHanded{}, err } box, err := seal.Seal(string(body), vaultPEM) if err != nil { return AccountsHanded{}, fmt.Errorf("delivery: the handed accounts could not be sealed to the vault's key: %w", err) } return AccountsHanded{From: from, To: KeyFingerprint(vaultPEM), Users: len(accounts), Box: box, 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 AccountsHanded, publicPEM, privatePEM string) ([]HandedAccount, error) { if h.To != KeyFingerprint(publicPEM) { return nil, fmt.Errorf("delivery: the handing is sealed to the key %s, not to this one (%s)", h.To, KeyFingerprint(publicPEM)) } plain, err := seal.Open(h.Box, privatePEM) if err != nil { return nil, fmt.Errorf("delivery: the handing does not open: %w", err) } var out []HandedAccount if err := json.Unmarshal([]byte(plain), &out); err != nil { return nil, fmt.Errorf("delivery: the handing holds no accounts: %w", err) } if len(out) != h.Users { return nil, fmt.Errorf("delivery: the handing says %d users and holds %d", h.Users, len(out)) } return out, nil } // sameHanded says whether two handings say the same: the same list, key, count and box, whatever zone their time is in. func sameHanded(a, b *AccountsHanded) bool { if a == nil || b == nil { return a == b } x, y := *a, *b x.At, y.At = time.Time{}, time.Time{} return x == y && a.At.Equal(b.At) }