From 282fb7e36170a22ad639ef169a2a97a19a4fe897 Mon Sep 17 00:00:00 2001 From: mleku Date: Thu, 30 Jan 2025 18:17:59 -0106 Subject: [PATCH] types, pubkey and signature codec --- doc/spec.md | 8 ++-- pkg/codec/codec.go | 13 ++++++ pkg/event/event.go | 14 +++++++ pkg/event/log.go | 9 ++++ pkg/event/types/log.go | 9 ++++ pkg/event/types/types.go | 44 ++++++++++++++++++++ pkg/event/types/types_test.go | 28 +++++++++++++ pkg/pubkey/log.go | 9 ++++ pkg/pubkey/pubkey.go | 74 +++++++++++++++++++++++++++++++++ pkg/pubkey/pubkey_test.go | 38 +++++++++++++++++ pkg/signature/log.go | 9 ++++ pkg/signature/signature.go | 74 +++++++++++++++++++++++++++++++++ pkg/signature/signature_test.go | 38 +++++++++++++++++ repos/readme.md | 3 ++ 14 files changed, 366 insertions(+), 4 deletions(-) create mode 100644 pkg/codec/codec.go create mode 100644 pkg/event/event.go create mode 100644 pkg/event/log.go create mode 100644 pkg/event/types/log.go create mode 100644 pkg/event/types/types.go create mode 100644 pkg/event/types/types_test.go create mode 100644 pkg/pubkey/log.go create mode 100644 pkg/pubkey/pubkey.go create mode 100644 pkg/pubkey/pubkey_test.go create mode 100644 pkg/signature/log.go create mode 100644 pkg/signature/signature.go create mode 100644 pkg/signature/signature_test.go create mode 100644 repos/readme.md diff --git a/doc/spec.md b/doc/spec.md index 4eeeb6e..88a2136 100644 --- a/doc/spec.md +++ b/doc/spec.md @@ -14,12 +14,12 @@ So, this is how realy events look: \n key:value;extra;...\n // zero or more line separated, fields cannot contain a semicolon, end with newline instead of semicolon, key lowercase alphanumeric, first alpha, only key is mandatory, only reserved is `content` content: // literally this word on one line -\n // any number of further line breaks, last line without break is signature - +\n // any number of further line breaks, last line is signature +\n ``` -The canonical form is exactly this, except the last linebreak and the base64 encoded signature, hashed using SHA256. There is no need for placing the event ID in the wire format either, so this is also the wire format. +The canonical form is exactly this, except for the signature and following linebreak, hashed with Blake2b The database stored form of this event should make use of an event ID hash to monotonic collision free serial table and an event table. -Event ID hashes will be encoded in URL-base64 where used in tags or mentioned in content with the prefix `event:`. Public keys must be prefixed with `pubkey:` Tag keys should be intelligible words and a specification for their structure should be befined by users of them and shared with other REALY devs. \ No newline at end of file +Event ID hashes will be encoded in URL-base64 where used in tags or mentioned in content with the prefix `event:`. Public keys must be prefixed with `pubkey:` Tag keys should be intelligible words and a specification for their structure should be defined by users of them and shared with other REALY devs. \ No newline at end of file diff --git a/pkg/codec/codec.go b/pkg/codec/codec.go new file mode 100644 index 0000000..a63ecd5 --- /dev/null +++ b/pkg/codec/codec.go @@ -0,0 +1,13 @@ +package codec + +// C is an interface for encoding and decoding that allows embedding encoders +// inside other encoders by the use of append for Marshal and slice for +// Unmarshal. +type C interface { + // Marshal data by appending it to the provided destination, and return the + // resultant slice. + Marshal(dst []byte) (result []byte, err error) + // Unmarshal the next expected data element from the provided slice and return + // the remainder after the expected separator. + Unmarshal(data []byte) (rem []byte, err error) +} diff --git a/pkg/event/event.go b/pkg/event/event.go new file mode 100644 index 0000000..ab921cc --- /dev/null +++ b/pkg/event/event.go @@ -0,0 +1,14 @@ +package event + +import ( + "protocol.realy.lol/pkg/event/types" +) + +type Event struct { + Type types.T + Pubkey []byte + Timestamp int64 + Tags [][]byte + Content []byte + Signature []byte +} diff --git a/pkg/event/log.go b/pkg/event/log.go new file mode 100644 index 0000000..ee09483 --- /dev/null +++ b/pkg/event/log.go @@ -0,0 +1,9 @@ +package event + +import ( + "protocol.realy.lol/pkg/lol" +) + +var ( + log, chk, errorf = lol.Main.Log, lol.Main.Check, lol.Main.Errorf +) diff --git a/pkg/event/types/log.go b/pkg/event/types/log.go new file mode 100644 index 0000000..c4ab3a5 --- /dev/null +++ b/pkg/event/types/log.go @@ -0,0 +1,9 @@ +package types + +import ( + "protocol.realy.lol/pkg/lol" +) + +var ( + log, chk, errorf = lol.Main.Log, lol.Main.Check, lol.Main.Errorf +) diff --git a/pkg/event/types/types.go b/pkg/event/types/types.go new file mode 100644 index 0000000..62c16af --- /dev/null +++ b/pkg/event/types/types.go @@ -0,0 +1,44 @@ +package types + +import ( + "io" +) + +// A T is a type descriptor, that is terminated by a newline. +type T []byte + +// Marshal append the T to a slice and appends a terminal newline, and returns +// the result. +func (t *T) Marshal(dst []byte) (result []byte, err error) { + if t == nil { + return + } + result = append(append(dst, []byte(*t)...), '\n') + return +} + +// Unmarshal expects an identifier followed by a newline. If the buffer ends +// without a newline an EOF is returned. +func (t *T) Unmarshal(data []byte) (rem []byte, err error) { + rem = data + if t == nil { + err = errorf.E("can't unmarshal into nil types.T") + return + } + if len(rem) < 2 { + err = errorf.E("can't unmarshal nothing") + return + } + for i := range rem { + if rem[i] == '\n' { + // write read data up to the newline and return the remainder after + // the newline. + *t = rem[:i] + rem = rem[i+1:] + return + } + } + // a T must end with a newline or an io.EOF is returned. + err = io.EOF + return +} diff --git a/pkg/event/types/types_test.go b/pkg/event/types/types_test.go new file mode 100644 index 0000000..73a1515 --- /dev/null +++ b/pkg/event/types/types_test.go @@ -0,0 +1,28 @@ +package types + +import ( + "bytes" + "testing" +) + +func TestT_Marshal_Unmarshal(t *testing.T) { + typ := T("note") + var err error + var res []byte + if res, err = typ.Marshal(nil); chk.E(err) { + t.Fatal(err) + } + log.I.S(res) + t2 := new(T) + var rem []byte + if rem, err = t2.Unmarshal(res); chk.E(err) { + t.Fatal(err) + } + if len(rem) > 0 { + log.I.S(rem) + } + log.I.S(t2) + if !bytes.Equal(typ, *t2) { + t.Fatal("types.T did not encode/decode faithfully") + } +} diff --git a/pkg/pubkey/log.go b/pkg/pubkey/log.go new file mode 100644 index 0000000..3686355 --- /dev/null +++ b/pkg/pubkey/log.go @@ -0,0 +1,9 @@ +package pubkey + +import ( + "protocol.realy.lol/pkg/lol" +) + +var ( + log, chk, errorf = lol.Main.Log, lol.Main.Check, lol.Main.Errorf +) diff --git a/pkg/pubkey/pubkey.go b/pkg/pubkey/pubkey.go new file mode 100644 index 0000000..0381a6d --- /dev/null +++ b/pkg/pubkey/pubkey.go @@ -0,0 +1,74 @@ +package pubkey + +import ( + "bytes" + "crypto/ed25519" + "encoding/base64" + "io" +) + +const Len = 44 + +type P struct{ ed25519.PublicKey } + +func New(pk []byte) (p *P, err error) { + if len(pk) != ed25519.PublicKeySize { + err = errorf.E("invalid public key size: %d; require %d", + len(pk), ed25519.PublicKeySize) + return + } + p = &P{pk} + return +} + +func (p *P) Marshal(dst []byte) (result []byte, err error) { + result = dst + if p == nil || p.PublicKey == nil || len(p.PublicKey) == 0 { + err = errorf.E("nil/zero length pubkey") + return + } + if len(p.PublicKey) != ed25519.PublicKeySize { + err = errorf.E("invalid public key length %d; require %d '%0x'", + len(p.PublicKey), ed25519.PublicKeySize, p.PublicKey) + return + } + buf := bytes.NewBuffer(result) + w := base64.NewEncoder(base64.URLEncoding, buf) + if _, err = w.Write(p.PublicKey); chk.E(err) { + return + } + if err = w.Close(); chk.E(err) { + return + } + result = append(buf.Bytes(), '\n') + return +} + +func (p *P) Unmarshal(data []byte) (rem []byte, err error) { + rem = data + if p == nil { + err = errorf.E("can't unmarshal into nil types.T") + return + } + if len(rem) < 2 { + err = errorf.E("can't unmarshal nothing") + return + } + for i := range rem { + if rem[i] == '\n' { + if i != Len { + err = errorf.E("invalid encoded pubkey length %d; require %d '%0x'", + i, Len, rem[:i]) + return + } + p.PublicKey = make([]byte, ed25519.PublicKeySize) + if _, err = base64.URLEncoding.Decode(p.PublicKey, rem[:i]); chk.E(err) { + return + } + rem = rem[i+1:] + return + } + } + err = io.EOF + return +} diff --git a/pkg/pubkey/pubkey_test.go b/pkg/pubkey/pubkey_test.go new file mode 100644 index 0000000..336c973 --- /dev/null +++ b/pkg/pubkey/pubkey_test.go @@ -0,0 +1,38 @@ +package pubkey + +import ( + "bytes" + "crypto/ed25519" + "crypto/rand" + "testing" +) + +func TestP_Marshal_Unmarshal(t *testing.T) { + pk := make([]byte, ed25519.PublicKeySize) + var err error + if _, err = rand.Read(pk); chk.E(err) { + t.Fatal(err) + } + log.I.S(pk) + var p *P + if p, err = New(pk); chk.E(err) { + t.Fatal(err) + } + var o []byte + if o, err = p.Marshal(nil); chk.E(err) { + t.Fatal(err) + } + log.I.F("%d %s", len(o), o) + p2 := &P{} + var rem []byte + if rem, err = p2.Unmarshal(o); chk.E(err) { + t.Fatal(err) + } + if len(rem) > 0 { + log.I.F("%d %s", len(rem), rem) + } + log.I.S(p2.PublicKey) + if !bytes.Equal(pk, p2.PublicKey) { + t.Fatal("public key did not encode/decode faithfully") + } +} diff --git a/pkg/signature/log.go b/pkg/signature/log.go new file mode 100644 index 0000000..2d40ac2 --- /dev/null +++ b/pkg/signature/log.go @@ -0,0 +1,9 @@ +package signature + +import ( + "protocol.realy.lol/pkg/lol" +) + +var ( + log, chk, errorf = lol.Main.Log, lol.Main.Check, lol.Main.Errorf +) diff --git a/pkg/signature/signature.go b/pkg/signature/signature.go new file mode 100644 index 0000000..8e760af --- /dev/null +++ b/pkg/signature/signature.go @@ -0,0 +1,74 @@ +package signature + +import ( + "bytes" + "crypto/ed25519" + "encoding/base64" + "io" +) + +const Len = 88 + +type S struct{ Signature []byte } + +func New(sig []byte) (p *S, err error) { + if len(sig) != ed25519.SignatureSize { + err = errorf.E("invalid signature size: %d; require %d", + len(sig), ed25519.SignatureSize) + return + } + p = &S{sig} + return +} + +func (p *S) Marshal(dst []byte) (result []byte, err error) { + result = dst + if p == nil || p.Signature == nil || len(p.Signature) == 0 { + err = errorf.E("nil/zero length signature") + return + } + if len(p.Signature) != ed25519.SignatureSize { + err = errorf.E("invalid signature length %d; require %d '%0x'", + len(p.Signature), ed25519.SignatureSize, p.Signature) + return + } + buf := bytes.NewBuffer(result) + w := base64.NewEncoder(base64.URLEncoding, buf) + if _, err = w.Write(p.Signature); chk.E(err) { + return + } + if err = w.Close(); chk.E(err) { + return + } + result = append(buf.Bytes(), '\n') + return +} + +func (p *S) Unmarshal(data []byte) (rem []byte, err error) { + rem = data + if p == nil { + err = errorf.E("can't unmarshal into nil types.T") + return + } + if len(rem) < 2 { + err = errorf.E("can't unmarshal nothing") + return + } + for i := range rem { + if rem[i] == '\n' { + if i != Len { + err = errorf.E("invalid encoded signature length %d; require %d '%0x'", + i, Len, rem[:i]) + return + } + p.Signature = make([]byte, ed25519.SignatureSize) + if _, err = base64.URLEncoding.Decode(p.Signature, rem[:i]); chk.E(err) { + return + } + rem = rem[i+1:] + return + } + } + err = io.EOF + return +} diff --git a/pkg/signature/signature_test.go b/pkg/signature/signature_test.go new file mode 100644 index 0000000..fdf74f3 --- /dev/null +++ b/pkg/signature/signature_test.go @@ -0,0 +1,38 @@ +package signature + +import ( + "bytes" + "crypto/ed25519" + "crypto/rand" + "testing" +) + +func TestS_Marshal_Unmarshal(t *testing.T) { + sig := make([]byte, ed25519.SignatureSize) + var err error + if _, err = rand.Read(sig); chk.E(err) { + t.Fatal(err) + } + log.I.S(sig) + var s *S + if s, err = New(sig); chk.E(err) { + t.Fatal(err) + } + var o []byte + if o, err = s.Marshal(nil); chk.E(err) { + t.Fatal(err) + } + log.I.F("%d %s", len(o), o) + p2 := &S{} + var rem []byte + if rem, err = p2.Unmarshal(o); chk.E(err) { + t.Fatal(err) + } + if len(rem) > 0 { + log.I.F("%d %s", len(rem), rem) + } + log.I.S(p2.Signature) + if !bytes.Equal(sig, p2.Signature) { + t.Fatal("signature did not encode/decode faithfully") + } +} diff --git a/repos/readme.md b/repos/readme.md new file mode 100644 index 0000000..4f8f4c0 --- /dev/null +++ b/repos/readme.md @@ -0,0 +1,3 @@ +# relays + +relay implementations for various subprotocols \ No newline at end of file