package delivery import ( "bytes" "encoding/json" "sort" "strconv" "git.novox.be/novox/mesh-sdk/go/catalogue" ) // Seals is, by slot, each value one composition carries sealed to its node's key (issue 1126, C1): a file's `sealed` // a vault box (catalogue.VaultSealedField) and each entry of a file's secrets (SecretsField), as their JSON text (a box is carried as its JSON, a sealed value as // the string it is). A slot is one field of one resource of the composition's module (SealSlot): a box is reused only // at the slot that holds it, never for another resource whose value opens to the same (a provider's grant and its // consumer's secret on one node hold one value). type Seals map[string]string // SealedFields are the fields of a resource that carry a value sealed to the node's key. var SealedFields = []string{"sealed", catalogue.VaultSealedField} // SealSlot is the slot of one sealed field of one resource, by the resource's id (its place, `#`, when it has none). // The module is the composition's: a slot is never compared across compositions. func SealSlot(resource, field string) string { return resource + "#" + field } // SecretsField is the field of a file that carries, by name, each sealed value its content asks for // (${secret:}, catalogue's intoFile): each entry is a slot of its own (SecretSlotField), so a secret sealed again // is judged by the value it opens to, entry by entry, like a file's `sealed` (issue 1126, C1; the seed-7 probe: about 45 // files on ace and novox differed by their secrets' bytes alone). const SecretsField = "secrets" // SecretSlotField is the field part of the slot of one entry of a file's secrets (SealSlot(id, SecretSlotField(name))). func SecretSlotField(name string) string { return SecretsField + "." + name } // sealAt is one sealed value of a resource: its slot's field, its place (a top-level field, or an entry of the // secrets), and its raw JSON. type sealAt struct { field, entry string raw json.RawMessage } // sealsAt is every sealed value of one resource's fields, in a fixed order: SealedFields, then the secrets by name. func sealsAt(fields map[string]json.RawMessage) []sealAt { var out []sealAt for _, field := range SealedFields { if f, has := fields[field]; has { out = append(out, sealAt{field: field, raw: f}) } } var secrets map[string]json.RawMessage if f, has := fields[SecretsField]; has && json.Unmarshal(f, &secrets) == nil { names := make([]string, 0, len(secrets)) for name := range secrets { names = append(names, name) } sort.Strings(names) for _, name := range names { out = append(out, sealAt{field: SecretSlotField(name), entry: name, raw: secrets[name]}) } } return out } // SealsIn is every sealed value c carries, by slot; nil when it carries none. func SealsIn(c Composed) Seals { var out Seals for i, raw := range c.Resources { id, fields, ok := sealedFieldsOf(raw, i) if !ok { continue } for _, s := range sealsAt(fields) { if text := sealedText(s.raw); text != "" { if out == nil { out = Seals{} } out[SealSlot(id, s.field)] = text } } } return out } // KeepSeals is c with each sealed value replaced by the one kept at the same slot, where same says the two open to the // same value (known and same): a value the vault sealed again is then the box that runs, so the composition is the // one that runs (ADR 0547 ยง3, issue 1126). A slot kept holds nothing for, or whose value differs or cannot be opened, // keeps c's. Every other byte of a resource is kept as it is; c itself is not changed. func KeepSeals(c Composed, kept Seals, same func(a, b string) (same, known bool)) Composed { if len(kept) == 0 || same == nil { return c } out := c out.Resources = nil for i, raw := range c.Resources { id, fields, ok := sealedFieldsOf(raw, i) if ok { for _, s := range sealsAt(fields) { ours := sealedText(s.raw) theirs, held := kept[SealSlot(id, s.field)] if !held || ours == "" || theirs == ours { continue } if opens, known := same(ours, theirs); !known || !opens { continue } value := json.RawMessage(theirs) if s.raw[0] == '"' { value, _ = json.Marshal(theirs) } if s.entry == "" { if swapped, ok := withField(raw, s.field, value); ok { raw = swapped } continue } // An entry of the secrets: swapped inside the secrets as they stand in raw, then the secrets in raw. var now map[string]json.RawMessage if json.Unmarshal(raw, &now) != nil { continue } if secrets, ok := withField(now[SecretsField], s.entry, value); ok { if swapped, ok := withField(raw, SecretsField, secrets); ok { raw = swapped } } } } out.Resources = append(out.Resources, raw) } return out } // sealedFieldsOf is a resource's id (`#` when it has none) and its top-level fields, false when it is no object. func sealedFieldsOf(raw json.RawMessage, i int) (string, map[string]json.RawMessage, bool) { var fields map[string]json.RawMessage if json.Unmarshal(raw, &fields) != nil { return "", nil, false } var id string if f, ok := fields["id"]; !ok || json.Unmarshal(f, &id) != nil || id == "" { id = "#" + strconv.Itoa(i) } return id, fields, true } // sealedText is a sealed field as its text: a string as it is, a box as its JSON. func sealedText(f json.RawMessage) string { var text string if json.Unmarshal(f, &text) == nil { return text } var compact bytes.Buffer if json.Compact(&compact, f) != nil { return string(f) } return compact.String() } // withField is the object raw with its top-level field replaced by value, every other byte as it was (so a composition // whose sealed values are those that run is the one that runs, byte for byte); false when raw is no object holding it. func withField(raw json.RawMessage, field string, value json.RawMessage) (json.RawMessage, bool) { d := json.NewDecoder(bytes.NewReader(raw)) if t, err := d.Token(); err != nil || t != json.Delim('{') { return nil, false } for d.More() { t, err := d.Token() if err != nil { return nil, false } key, _ := t.(string) before := d.InputOffset() var skip json.RawMessage if err := d.Decode(&skip); err != nil { return nil, false } if key != field { continue } after := d.InputOffset() // The span holds the colon, any space and the value: kept up to the colon, the value replaced. span := raw[before:after] colon := bytes.IndexByte(span, ':') if colon < 0 { return nil, false } out := make([]byte, 0, len(raw)-len(span)+colon+1+len(value)) out = append(out, raw[:int(before)+colon+1]...) out = append(out, value...) out = append(out, raw[after:]...) return out, true } return nil, false }