extension.CheckNote, NewNote and Note apply the rules of spec v0.11 24.1, and extension.Standard registers the note for the noncritical array of PUBLIC_HEADER only. Header.PublicNote reads it, and header_binding ties it to the control: a note changed after writing fails step 15. The CLI writes it with -note and shows it as text of the creator, with the warning that nobody can check it before the date. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>v0.11
parent
6cac49b5d8
commit
2142fcb9dd
@ -0,0 +1,85 @@
|
||||
package capsule_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
datekeys "g.activething.com/go/DateKeys"
|
||||
"g.activething.com/go/DateKeys/capsule"
|
||||
"g.activething.com/go/DateKeys/extension"
|
||||
"g.activething.com/go/DateKeys/internal/testkit"
|
||||
)
|
||||
|
||||
// Spec v0.11 §24.1, §62.1 rule 23: the public note is in PUBLIC_HEADER, in
|
||||
// clear, and header_binding ties it to the control: changing it makes step 15
|
||||
// fail.
|
||||
func TestPublicNote(t *testing.T) {
|
||||
opts := files3(t)
|
||||
opts.PublicNote = "Cartas del viaje a Lisboa"
|
||||
var dkc bytes.Buffer
|
||||
if _, err := capsule.EncryptFiles(&dkc, []capsule.Source{source("nota.txt", "Hola.\n")}, opts); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
in, err := capsule.Inspect(bytes.NewReader(dkc.Bytes()), capsule.InspectOptions{Registry: testkit.Registry()})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if note, ok := in.Header.PublicNote(); !ok || note != "Cartas del viaje a Lisboa" {
|
||||
t.Errorf("the note: %q %v", note, ok)
|
||||
}
|
||||
// The same bytes with another letter in the note: it still inspects, and
|
||||
// Open fails at step 15 with ERR_HEADER_BINDING.
|
||||
changed := bytes.Replace(dkc.Bytes(), []byte("Lisboa"), []byte("Lisbon"), 1)
|
||||
if bytes.Equal(changed, dkc.Bytes()) {
|
||||
t.Fatal("the note is not in clear in the capsule")
|
||||
}
|
||||
o := defaultOpen(1000)
|
||||
o.Sink = &testkit.MemorySink{}
|
||||
if _, err := capsule.Open(context.Background(), nil, bytes.NewReader(changed), o); !errors.Is(err, datekeys.ErrHeaderBinding) {
|
||||
t.Errorf("a note changed: %v", err)
|
||||
}
|
||||
if _, err := capsule.Open(context.Background(), nil, bytes.NewReader(dkc.Bytes()), o); err != nil {
|
||||
t.Errorf("the capsule with its note: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicNoteRules(t *testing.T) {
|
||||
for name, text := range map[string]string{
|
||||
"a tab": "a\tb",
|
||||
"a line feed": "a\nb",
|
||||
"a space at the end": "a ",
|
||||
"a space at the start": " a",
|
||||
"too long": strings.Repeat("a", extension.MaxNoteLen+1),
|
||||
"not UTF-8": "\xff",
|
||||
} {
|
||||
if err := extension.CheckNote(text); err == nil {
|
||||
t.Errorf("%s: accepted", name)
|
||||
}
|
||||
opts := files3(t)
|
||||
opts.PublicNote = text
|
||||
if _, err := capsule.EncryptFiles(&bytes.Buffer{}, []capsule.Source{source("a", "x")}, opts); err == nil {
|
||||
t.Errorf("%s: written", name)
|
||||
}
|
||||
}
|
||||
if err := extension.CheckNote(strings.Repeat("a", extension.MaxNoteLen)); err != nil {
|
||||
t.Errorf("1024 bytes: %v", err)
|
||||
}
|
||||
// An unusable note shows nothing.
|
||||
if _, ok := extension.Note([]extension.Extension{{ID: extension.NoteID, Version: 1, Data: []byte("a\nb")}}); ok {
|
||||
t.Error("an unusable note was shown")
|
||||
}
|
||||
// The registry knows it only in PUBLIC_HEADER, noncritical.
|
||||
var reg extension.Standard
|
||||
if !extension.KnownIn(reg, extension.NoteID, 1, extension.PublicHeader, extension.Noncritical) ||
|
||||
extension.KnownIn(reg, extension.NoteID, 1, extension.Control, extension.Noncritical) ||
|
||||
extension.KnownIn(reg, extension.NoteID, 1, extension.PublicHeader, extension.Critical) ||
|
||||
extension.KnownIn(reg, extension.NoteID, 2, extension.PublicHeader, extension.Noncritical) {
|
||||
t.Error("registration of datekeys.note")
|
||||
}
|
||||
if us := extension.CheckNoncriticalIn(extension.PublicHeader, []extension.Extension{{ID: extension.NoteID, Version: 1, Data: []byte("a\tb")}}, reg); len(us) != 1 {
|
||||
t.Errorf("the note with a tab is not unusable: %v", us)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,101 @@
|
||||
package extension
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"unicode/utf8"
|
||||
|
||||
datekeys "g.activething.com/go/DateKeys"
|
||||
"g.activething.com/go/DateKeys/internal/pathrule"
|
||||
)
|
||||
|
||||
// The extensions that spec v0.11 registers (§72).
|
||||
const (
|
||||
// NoteID is the public note of a capsule, in the noncritical array of
|
||||
// PUBLIC_HEADER (§24.1), version 1: its data is the text in UTF-8, with
|
||||
// no CBOR around it.
|
||||
NoteID = "datekeys.note"
|
||||
// CapsuleID is the extension of a .dkk that says what its capsule is and
|
||||
// where to find it (§44.1), version 1.
|
||||
CapsuleID = "datekeys.capsule"
|
||||
// MaxNoteLen is the longest public note, in bytes.
|
||||
MaxNoteLen = 1024
|
||||
)
|
||||
|
||||
// CheckNote checks the text of a public note with the rules of spec §24.1:
|
||||
// from 1 to 1024 bytes of valid UTF-8 that meet the rules of the declared
|
||||
// author of §29.6, one line without tabs and without spaces at the ends.
|
||||
func CheckNote(text string) error {
|
||||
switch {
|
||||
case text == "" || len(text) > MaxNoteLen:
|
||||
return fmt.Errorf("a public note of %d bytes, not 1 to %d: %w", len(text), MaxNoteLen, datekeys.ErrExtensionDataInvalid)
|
||||
case !utf8.ValidString(text):
|
||||
return fmt.Errorf("a public note that is not valid UTF-8: %w", datekeys.ErrExtensionDataInvalid)
|
||||
}
|
||||
if err := pathrule.CheckAuthor(text); err != nil {
|
||||
return fmt.Errorf("a public note that breaks the rules of text: %v: %w", err, datekeys.ErrExtensionDataInvalid)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewNote returns the public note extension for text, which must pass
|
||||
// CheckNote. A note is public: whoever has the .dkc reads it before the date,
|
||||
// and with the date it can identify someone (spec §24.1).
|
||||
func NewNote(text string) (Extension, error) {
|
||||
if err := CheckNote(text); err != nil {
|
||||
return Extension{}, err
|
||||
}
|
||||
return New(NoteID, 1, []byte(text))
|
||||
}
|
||||
|
||||
// Note returns the text of the public note among the noncritical extensions
|
||||
// of a PUBLIC_HEADER, and whether there is one that is usable: "", false when
|
||||
// there is none, or when its data breaks the rules of §24.1, in which case
|
||||
// a reader treats it as unusable and shows nothing (spec §54).
|
||||
func Note(noncritical []Extension) (string, bool) {
|
||||
for _, e := range noncritical {
|
||||
if e.ID == NoteID && e.Version == 1 {
|
||||
if e.Data == nil || CheckNote(string(e.Data)) != nil {
|
||||
return "", false
|
||||
}
|
||||
return string(e.Data), true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// Standard is the Registry of the extensions that spec v0.11 defines: the
|
||||
// public note in PUBLIC_HEADER and datekeys.capsule in a .dkk, both
|
||||
// noncritical. It validates their data, as spec §54 asks of a reader that
|
||||
// knows an extension, and registers each only where §72 does.
|
||||
type Standard struct{}
|
||||
|
||||
// Known reports whether (id, version) is one of the two.
|
||||
func (Standard) Known(id string, version uint64) bool {
|
||||
return version == 1 && (id == NoteID || id == CapsuleID)
|
||||
}
|
||||
|
||||
// RegisteredIn reports where the extensions are registered (spec §72).
|
||||
func (Standard) RegisteredIn(id string, version uint64, obj Object, arr Array) bool {
|
||||
if arr != Noncritical || version != 1 {
|
||||
return false
|
||||
}
|
||||
return id == NoteID && obj == PublicHeader || id == CapsuleID && obj == AccessKey
|
||||
}
|
||||
|
||||
// ValidateData checks the data of a note. The data of datekeys.capsule has
|
||||
// its own decoder, in package accesskey, which a caller uses to read it:
|
||||
// this one checks only that it is present.
|
||||
func (Standard) ValidateData(e Extension) error {
|
||||
switch e.ID {
|
||||
case NoteID:
|
||||
if e.Data == nil {
|
||||
return fmt.Errorf("a public note without data: %w", datekeys.ErrExtensionDataInvalid)
|
||||
}
|
||||
return CheckNote(string(e.Data))
|
||||
case CapsuleID:
|
||||
if e.Data == nil {
|
||||
return fmt.Errorf("datekeys.capsule without data: %w", datekeys.ErrExtensionDataInvalid)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Loading…
Reference in new issue