The MAY of spec §62.1 rule 26, a local reminder of the instant the capsule can be opened: encrypt -reminder writes FILE.dkc.recordatorio.ics, an iCalendar event (RFC 5545) at round_time with an alarm, in Spanish, saying what opening will take and where the recovery annex is. Lines end in CRLF and fold at 75 octets without cutting a character; the name is escaped as TEXT. The UID is a random UUID, so that it names no capsule; the CLI says that a calendar that syncs with a server learns the name and the date. The test compares the file byte for byte with a text computed apart from the code, from the RFC. The traceability said that the CLI did not implement rules 26 and 27, which it does since aefc8f6; the row now names that code and its tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>v0.16
parent
0eb5aa0c2b
commit
7e6a03d464
@ -0,0 +1,107 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// reminderSuffix names the calendar file that encrypt -reminder writes next
|
||||
// to the capsule: FILE.dkc.recordatorio.ics, as the recovery annex is
|
||||
// FILE.dkc.recuperacion.txt.
|
||||
const reminderSuffix = ".recordatorio.ics"
|
||||
|
||||
// reminder is a local reminder of the instant a capsule can be opened, the
|
||||
// MAY of spec §62.1 rule 26: an iCalendar event (RFC 5545) at round_time,
|
||||
// which any calendar imports. It says what opening will take, as encrypt
|
||||
// does when it seals. It holds the name of the capsule and its date, and
|
||||
// nothing secret; a calendar that syncs with a server tells it both.
|
||||
type reminder struct {
|
||||
// Name is the file name of the capsule, and Round and UnlockAt its round
|
||||
// and round_time. TimeAndKey says whether it needs one of its
|
||||
// credentials too.
|
||||
Name string
|
||||
Round uint64
|
||||
UnlockAt time.Time
|
||||
TimeAndKey bool
|
||||
// UID identifies the event, a random UUID, so that it names no capsule;
|
||||
// Stamp is when the event was made.
|
||||
UID string
|
||||
Stamp time.Time
|
||||
}
|
||||
|
||||
// newUID returns a random UUID of version 4 (RFC 9562).
|
||||
func newUID() (string, error) {
|
||||
var b [16]byte
|
||||
if _, err := rand.Read(b[:]); err != nil {
|
||||
return "", err
|
||||
}
|
||||
b[6] = b[6]&0x0f | 0x40
|
||||
b[8] = b[8]&0x3f | 0x80
|
||||
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]), nil
|
||||
}
|
||||
|
||||
// calendarText escapes a TEXT value of RFC 5545 (3.3.11): a backslash, a
|
||||
// semicolon, a comma and a line feed; a carriage return goes.
|
||||
var calendarText = strings.NewReplacer(`\`, `\\`, ";", `\;`, ",", `\,`, "\n", `\n`, "\r", "")
|
||||
|
||||
// calendarTime writes t in UTC, as the DATE-TIME of RFC 5545 with a Z.
|
||||
func calendarTime(t time.Time) string { return t.UTC().Format("20060102T150405Z") }
|
||||
|
||||
// foldLine ends a content line with CRLF, folded at 75 octets (RFC 5545,
|
||||
// 3.1): each continuation starts with a space, and no UTF-8 sequence is cut.
|
||||
func foldLine(b *strings.Builder, line string) {
|
||||
limit := 75
|
||||
for len(line) > limit {
|
||||
cut := limit
|
||||
for cut > 0 && !utf8.RuneStart(line[cut]) {
|
||||
cut--
|
||||
}
|
||||
b.WriteString(line[:cut])
|
||||
b.WriteString("\r\n ")
|
||||
line = line[cut:]
|
||||
limit = 74
|
||||
}
|
||||
b.WriteString(line)
|
||||
b.WriteString("\r\n")
|
||||
}
|
||||
|
||||
// ics is the calendar file of the reminder, in Spanish, as the annex is.
|
||||
func (r reminder) ics() string {
|
||||
summary := "Ya se puede abrir la cápsula DateKeys «" + r.Name + "»"
|
||||
needs := "el fichero .dkc"
|
||||
if r.TimeAndKey {
|
||||
needs = "el fichero .dkc y una de sus llaves"
|
||||
}
|
||||
description := fmt.Sprintf("Desde este momento se puede abrir «%s». Hace falta %s, y la firma de drand de la ronda %d, "+
|
||||
"que drand publica ahora: si un día ya no la sirve, un archivo de firmas o un servicio de caché tiene que haberla guardado. "+
|
||||
"Las instrucciones para abrirla sin DateKeys están en «%s».",
|
||||
r.Name, needs, r.Round, r.Name+".recuperacion.txt")
|
||||
var b strings.Builder
|
||||
for _, line := range []string{
|
||||
"BEGIN:VCALENDAR",
|
||||
"VERSION:2.0",
|
||||
"PRODID:-//DateKeys//datekeys-go//ES",
|
||||
"CALSCALE:GREGORIAN",
|
||||
"BEGIN:VEVENT",
|
||||
"UID:" + r.UID,
|
||||
"DTSTAMP:" + calendarTime(r.Stamp),
|
||||
"DTSTART:" + calendarTime(r.UnlockAt),
|
||||
"DTEND:" + calendarTime(r.UnlockAt.Add(30*time.Minute)),
|
||||
"SUMMARY:" + calendarText.Replace(summary),
|
||||
"DESCRIPTION:" + calendarText.Replace(description),
|
||||
"TRANSP:TRANSPARENT",
|
||||
"BEGIN:VALARM",
|
||||
"ACTION:DISPLAY",
|
||||
"TRIGGER:PT0S",
|
||||
"DESCRIPTION:" + calendarText.Replace(summary),
|
||||
"END:VALARM",
|
||||
"END:VEVENT",
|
||||
"END:VCALENDAR",
|
||||
} {
|
||||
foldLine(&b, line)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
@ -0,0 +1,94 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"g.activething.com/go/DateKeys/profile"
|
||||
)
|
||||
|
||||
// The calendar file of -reminder, byte for byte (spec §62.1 rule 26, RFC
|
||||
// 5545): CRLF, lines folded at 75 octets without cutting a character, and a
|
||||
// name whose semicolon, comma and backslash are escaped. The expected text
|
||||
// was computed apart from this code, from the RFC.
|
||||
func TestReminderICS(t *testing.T) {
|
||||
r := reminder{
|
||||
Name: "carta; de, mamá\\.dkc", Round: 1000, TimeAndKey: true,
|
||||
UnlockAt: time.Date(2023, 8, 23, 15, 59, 24, 0, time.UTC),
|
||||
UID: "0f1e2d3c-4b5a-4697-8877-665544332211",
|
||||
Stamp: time.Date(2026, 10, 7, 12, 0, 0, 0, time.UTC),
|
||||
}
|
||||
want :=
|
||||
"BEGIN:VCALENDAR\r\n" +
|
||||
"VERSION:2.0\r\n" +
|
||||
"PRODID:-//DateKeys//datekeys-go//ES\r\n" +
|
||||
"CALSCALE:GREGORIAN\r\n" +
|
||||
"BEGIN:VEVENT\r\n" +
|
||||
"UID:0f1e2d3c-4b5a-4697-8877-665544332211\r\n" +
|
||||
"DTSTAMP:20261007T120000Z\r\n" +
|
||||
"DTSTART:20230823T155924Z\r\n" +
|
||||
"DTEND:20230823T162924Z\r\n" +
|
||||
"SUMMARY:Ya se puede abrir la cápsula DateKeys «carta\\; de\\, mamá\\\\.dkc»\r\n" +
|
||||
"DESCRIPTION:Desde este momento se puede abrir «carta\\; de\\, mamá\\\\.dkc».\r\n" +
|
||||
" Hace falta el fichero .dkc y una de sus llaves\\, y la firma de drand de l\r\n" +
|
||||
" a ronda 1000\\, que drand publica ahora: si un día ya no la sirve\\, un arc\r\n" +
|
||||
" hivo de firmas o un servicio de caché tiene que haberla guardado. Las ins\r\n" +
|
||||
" trucciones para abrirla sin DateKeys están en «carta\\; de\\, mamá\\\\.dkc.\r\n" +
|
||||
" recuperacion.txt».\r\n" +
|
||||
"TRANSP:TRANSPARENT\r\n" +
|
||||
"BEGIN:VALARM\r\n" +
|
||||
"ACTION:DISPLAY\r\n" +
|
||||
"TRIGGER:PT0S\r\n" +
|
||||
"DESCRIPTION:Ya se puede abrir la cápsula DateKeys «carta\\; de\\, mamá\\\\.d\r\n" +
|
||||
" kc»\r\n" +
|
||||
"END:VALARM\r\n" +
|
||||
"END:VEVENT\r\n" +
|
||||
"END:VCALENDAR\r\n"
|
||||
got := r.ics()
|
||||
if got != want {
|
||||
t.Fatalf("got\n%s\nwant\n%s", got, want)
|
||||
}
|
||||
for _, line := range strings.Split(strings.TrimSuffix(got, "\r\n"), "\r\n") {
|
||||
if len(line) > 75 || !utf8.ValidString(line) {
|
||||
t.Errorf("a line of %d octets: %q", len(line), line)
|
||||
}
|
||||
}
|
||||
if id, err := newUID(); err != nil || len(id) != 36 || id[14] != '4' || !strings.ContainsRune("89ab", rune(id[19])) {
|
||||
t.Errorf("a UUID of version 4: %q, %v", id, err)
|
||||
}
|
||||
}
|
||||
|
||||
// encrypt -reminder writes the calendar next to the capsule and says what it
|
||||
// reveals; without the option, nothing.
|
||||
func TestEncryptReminder(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
in := filepath.Join(dir, "carta.txt")
|
||||
os.WriteFile(in, []byte("para dentro de mucho"), 0o600)
|
||||
p := profile.Quicknet()
|
||||
genesis := time.Unix(p.GenesisTime, 0)
|
||||
at := time.Unix(p.GenesisTime+999*3, 0).UTC().Format(time.RFC3339) // round 1000
|
||||
dkc := filepath.Join(dir, "carta.dkc")
|
||||
_, stderr, err := cli(t, genesis, "encrypt", "-at", at, "-in", in, "-out", dkc, "-reminder")
|
||||
if err != nil {
|
||||
t.Fatalf("encrypt -reminder: %v\n%s", err, stderr)
|
||||
}
|
||||
b, err := os.ReadFile(dkc + ".recordatorio.ics")
|
||||
if err != nil || !strings.Contains(string(b), "\r\nDTSTART:20230823T155924Z\r\n") || !strings.Contains(string(b), "«carta.dkc»") ||
|
||||
strings.Contains(string(b), "una de sus llaves") {
|
||||
t.Fatalf("the reminder: %v\n%s", err, b)
|
||||
}
|
||||
if !strings.Contains(stderr, " reminder "+dkc+".recordatorio.ics: a calendar event at 2023-08-23T15:59:24Z") {
|
||||
t.Errorf("stderr:\n%s", stderr)
|
||||
}
|
||||
other := filepath.Join(dir, "otra.dkc")
|
||||
if _, _, err := cli(t, genesis, "encrypt", "-at", at, "-in", in, "-out", other); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stat(other + ".recordatorio.ics"); err == nil {
|
||||
t.Error("a reminder without -reminder")
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Loading…
Reference in new issue