diff options
Diffstat (limited to 'yamn/encoder/encoder_test.go')
| -rw-r--r-- | yamn/encoder/encoder_test.go | 130 |
1 files changed, 130 insertions, 0 deletions
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]) +} |
