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) } } // Review: spec §72 forbids an encoder to write a registered extension where // it is not registered. A note given as an extension is checked like // PublicNote, and no array of a capsule but the noncritical one of // PUBLIC_HEADER takes it. In CONTROL_CBOR, critical, it would have inspected // well and failed after the date at step 14. datekeys.capsule goes in none. func TestRegisteredExtensionsWhereRegistered(t *testing.T) { note := extension.Extension{ID: extension.NoteID, Version: 1, Data: []byte("Cartas")} tab := extension.Extension{ID: extension.NoteID, Version: 1, Data: []byte("a\tb")} capsuleExt := extension.Extension{ID: extension.CapsuleID, Version: 1, Data: []byte{0xa0}} for name, set := range map[string]func(*capsule.EncryptOptions){ "a note with a tab": func(o *capsule.EncryptOptions) { o.Noncritical = []extension.Extension{tab} }, "a note in critical": func(o *capsule.EncryptOptions) { o.Critical = []extension.Extension{note} }, "a note in the control": func(o *capsule.EncryptOptions) { o.ControlNoncritical = []extension.Extension{note} }, "a note in the control, critical": func(o *capsule.EncryptOptions) { o.ControlCritical = []extension.Extension{note} }, "a note in the head": func(o *capsule.EncryptOptions) { o.HeadNoncritical = []extension.Extension{note} }, "datekeys.capsule": func(o *capsule.EncryptOptions) { o.Noncritical = []extension.Extension{capsuleExt} }, } { opts := files3(t) set(&opts) if _, err := capsule.EncryptFiles(&bytes.Buffer{}, []capsule.Source{source("a", "x")}, opts); err == nil { t.Errorf("%s: written", name) } } opts := files3(t) opts.Noncritical = []extension.Extension{note} var dkc bytes.Buffer if _, err := capsule.EncryptFiles(&dkc, []capsule.Source{source("a", "x")}, opts); err != nil { t.Fatalf("a valid note as an extension: %v", err) } } // Review: a reader tells a note that breaks the rules of §24.1 from no note, // so that it can say that it does not show one. func TestUnusableNote(t *testing.T) { for _, c := range []struct { exts []extension.Extension want bool }{ {nil, false}, {[]extension.Extension{{ID: extension.NoteID, Version: 1, Data: []byte("Cartas")}}, false}, {[]extension.Extension{{ID: extension.NoteID, Version: 1, Data: []byte("a\tb")}}, true}, {[]extension.Extension{{ID: extension.NoteID, Version: 2, Data: []byte("a\tb")}}, false}, } { h := &capsule.Header{Noncritical: c.exts} if got := h.UnusableNote(); got != c.want { t.Errorf("%+v: %v, want %v", c.exts, got, c.want) } } }