diff options
| author | Gab <24553253+gabrix73@users.noreply.github.com> | 2026-08-14 20:39:22 +0200 |
|---|---|---|
| committer | Gab <24553253+gabrix73@users.noreply.github.com> | 2026-08-14 20:39:22 +0200 |
| commit | 43fbddf016f94f4ba006d82c9a67dca61b5852a1 (patch) | |
| tree | 70054d5fa8a34bb094b8c3ef8ddc9c1fcea3b3d5 /yamn/encoder | |
| parent | 994071243fc80990cf09e820b671006e9bd31c76 (diff) | |
| download | yamnweb-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.go | 11 | ||||
| -rw-r--r-- | yamn/encoder/encoder_test.go | 130 |
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]) +} |
