summaryrefslogtreecommitdiffstats
path: root/yamn/encoder
diff options
context:
space:
mode:
authorGab <24553253+gabrix73@users.noreply.github.com>2026-08-14 20:39:22 +0200
committerGab <24553253+gabrix73@users.noreply.github.com>2026-08-14 20:39:22 +0200
commit43fbddf016f94f4ba006d82c9a67dca61b5852a1 (patch)
tree70054d5fa8a34bb094b8c3ef8ddc9c1fcea3b3d5 /yamn/encoder
parent994071243fc80990cf09e820b671006e9bd31c76 (diff)
downloadyamnweb-43fbddf016f94f4ba006d82c9a67dca61b5852a1.tar.gz
yamnweb-43fbddf016f94f4ba006d82c9a67dca61b5852a1.tar.xz
yamnweb-43fbddf016f94f4ba006d82c9a67dca61b5852a1.zip
Complete Nym and YAMN transport integration
Diffstat (limited to 'yamn/encoder')
-rw-r--r--yamn/encoder/encoder.go11
-rw-r--r--yamn/encoder/encoder_test.go130
2 files changed, 136 insertions, 5 deletions
diff --git a/yamn/encoder/encoder.go b/yamn/encoder/encoder.go
index 5eacdf3..3b595b1 100644
--- a/yamn/encoder/encoder.go
+++ b/yamn/encoder/encoder.go
@@ -304,19 +304,20 @@ func slotData(kind byte, packetID, aesKey, info, tag []byte) []byte {
}
func encryptedHeader(key remailerKey, data []byte) []byte {
- var recipient, sender [32]byte
+ var recipient [32]byte
copy(recipient[:], key.publicKey)
- if _, err := io.ReadFull(rand.Reader, sender[:]); err != nil {
+ senderPublic, senderSecret, err := box.GenerateKey(rand.Reader)
+ if err != nil {
panic(err)
}
var nonce [24]byte
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
panic(err)
}
- sealed := box.Seal(nil, data, &nonce, &recipient, &sender)
+ sealed := box.Seal(nil, data, &nonce, &recipient, senderSecret)
b := make([]byte, headerBytes)
copy(b, key.keyID)
- copy(b[16:], sender[:])
+ copy(b[16:], senderPublic[:])
copy(b[48:], nonce[:])
copy(b[72:], sealed)
return b
@@ -370,7 +371,7 @@ func deterministic(p []byte, keys, ivs [][]byte, chainLen, hop int) {
for slot := top; slot <= bottom; slot++ {
fake := make([]byte, headerBytes)
use := bottom
- for i := bottom - slot + hop; i >= 0; i-- {
+ for i := bottom - slot + hop; i >= hop; i-- {
copy(fake, aesCTR(fake, keys[i], seqIV(ivs[i], use)))
use--
}
diff --git a/yamn/encoder/encoder_test.go b/yamn/encoder/encoder_test.go
index a6ea192..3509070 100644
--- a/yamn/encoder/encoder_test.go
+++ b/yamn/encoder/encoder_test.go
@@ -1,10 +1,16 @@
package encoder
import (
+ "bytes"
+ "crypto/rand"
"encoding/base64"
+ "encoding/binary"
+ "encoding/hex"
"os"
"strings"
"testing"
+
+ "golang.org/x/crypto/nacl/box"
)
func testKeyring(t *testing.T) string {
@@ -113,3 +119,127 @@ func TestEncodeMultiHop(t *testing.T) {
t.Fatal("multi-hop envelope was not produced")
}
}
+
+func TestEncodeMultiHopDecodesAtEveryRemailer(t *testing.T) {
+ keyring, secretKeys := testDecodeKeyring(t)
+ request := Request{
+ Kind: Email, PublicKeyring: keyring, Entry: "entry",
+ Chain: []string{"entry", "middle", "exit"}, From: "Anonymous <anon@example.org>",
+ To: "user@example.org", Subject: "three-hop compatibility", Body: "hello through three remailers",
+ }
+ result, err := Encode(request)
+ if err != nil {
+ t.Fatal(err)
+ }
+ packet := decodeArmoredPacket(t, result.Envelope)
+ wantPlain, err := composeMessage(request)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ for hop, name := range request.Chain {
+ data := openHeaderForTest(t, packet[:headerBytes], secretKeys[name])
+ if data[0] != 2 {
+ t.Fatalf("hop %d (%s): unexpected packet version %d", hop, name, data[0])
+ }
+ if !bytes.Equal(data[117:149], antiTag(packet)) {
+ t.Fatalf("hop %d (%s): anti-tag mismatch", hop, name)
+ }
+
+ switch data[1] {
+ case 0:
+ if hop == len(request.Chain)-1 {
+ t.Fatalf("hop %d (%s): exit encoded as intermediate", hop, name)
+ }
+ shiftHeadersUpForTest(packet)
+ encryptAll(packet, data[19:51], data[53:65])
+ case 1:
+ if hop != len(request.Chain)-1 {
+ t.Fatalf("hop %d (%s): intermediate encoded as exit", hop, name)
+ }
+ bodyLength := int(binary.LittleEndian.Uint32(data[87:91]))
+ if bodyLength != len(wantPlain) {
+ t.Fatalf("exit body length: got %d, want %d", bodyLength, len(wantPlain))
+ }
+ body := aesCTR(packet[maxChainLength*headerBytes:], data[19:51], data[53:69])
+ if !bytes.Equal(body[:bodyLength], wantPlain) {
+ t.Fatal("exit plaintext does not match the encoded message")
+ }
+ default:
+ t.Fatalf("hop %d (%s): unknown packet type %d", hop, name, data[1])
+ }
+ }
+}
+
+func testDecodeKeyring(t *testing.T) (string, map[string]*[32]byte) {
+ t.Helper()
+ path := t.TempDir() + "/pubring.mix"
+ secretKeys := make(map[string]*[32]byte)
+ var content strings.Builder
+ for i, name := range []string{"entry", "middle", "exit"} {
+ publicKey, secretKey, err := box.GenerateKey(rand.Reader)
+ if err != nil {
+ t.Fatal(err)
+ }
+ keyID := bytes.Repeat([]byte{byte(i + 1)}, 16)
+ content.WriteString(name + " " + name + "@example.org " + hex.EncodeToString(keyID) + " 4:0.2c E 2025-01-01 2099-12-31\n\n")
+ content.WriteString("-----Begin Mix Key-----\n" + hex.EncodeToString(keyID) + "\n" + hex.EncodeToString(publicKey[:]) + "\n-----End Mix Key-----\n")
+ secretKeys[name] = secretKey
+ }
+ if err := os.WriteFile(path, []byte(content.String()), 0600); err != nil {
+ t.Fatal(err)
+ }
+ return path, secretKeys
+}
+
+func decodeArmoredPacket(t *testing.T, envelope []byte) []byte {
+ t.Helper()
+ lines := strings.Split(string(envelope), "\n")
+ start := -1
+ for i, line := range lines {
+ if line == "-----BEGIN REMAILER MESSAGE-----" {
+ start = i + 3
+ break
+ }
+ }
+ if start < 0 {
+ t.Fatal("missing armored packet start")
+ }
+ var encoded strings.Builder
+ for _, line := range lines[start:] {
+ if line == "" || strings.HasPrefix(line, "-----END") {
+ break
+ }
+ encoded.WriteString(line)
+ }
+ packet, err := base64.StdEncoding.DecodeString(encoded.String())
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(packet) != messageBytes {
+ t.Fatalf("unexpected packet size: got %d, want %d", len(packet), messageBytes)
+ }
+ return packet
+}
+
+func openHeaderForTest(t *testing.T, header []byte, recipientSecret *[32]byte) []byte {
+ t.Helper()
+ var senderPublic [32]byte
+ copy(senderPublic[:], header[16:48])
+ var nonce [24]byte
+ copy(nonce[:], header[48:72])
+ data, ok := box.Open(nil, header[72:248], &nonce, &senderPublic, recipientSecret)
+ if !ok {
+ t.Fatal("header authentication failed")
+ }
+ if len(data) != encHeadBytes {
+ t.Fatalf("unexpected decoded header size: got %d, want %d", len(data), encHeadBytes)
+ }
+ return data
+}
+
+func shiftHeadersUpForTest(packet []byte) {
+ headersEnd := maxChainLength * headerBytes
+ copy(packet, packet[headerBytes:headersEnd])
+ clear(packet[headersEnd-headerBytes : headersEnd])
+}