diff options
| author | Gab Virebent <gabriel1@virebent.art> | 2026-07-11 13:37:23 +0200 |
|---|---|---|
| committer | Gab Virebent <gabriel1@virebent.art> | 2026-07-11 13:37:23 +0200 |
| commit | bec3734676faf3868ea225e0316e77ad5a7e4421 (patch) | |
| tree | 78bc62532fcdb9f42eb92c41f0841b5fdeeb063d /internal/nym/address.go | |
| parent | f21e4d8781c06c0686f4a8697ce0697f9bf6a5cc (diff) | |
| download | nymdrop-bec3734676faf3868ea225e0316e77ad5a7e4421.tar.gz nymdrop-bec3734676faf3868ea225e0316e77ad5a7e4421.tar.xz nymdrop-bec3734676faf3868ea225e0316e77ad5a7e4421.zip | |
Switch Nym WS transport to nym-client's binary protocol, drop second base64 layer
Client.Send now speaks nym-client's native binary websocket protocol
(tag || recipient || conn_id || data_len || data) instead of wrapping
the ciphertext in a JSON text message, which required base64-encoding
it a second time on top of the browser's own base64 layer.
The reader's self-address query also switched to the binary protocol:
nym-client picks text-vs-binary for every later "received" push based
on the format of the last request seen on a connection, so leaving the
reader in JSON/text mode would have made nym-client run a lossy UTF-8
conversion over the now-unencoded binary payload, corrupting it. Fixed
the binary Received-frame parsing along the way (previous code assumed
a fixed 16-byte tag with no length prefix, which never matched the real
protocol and was never exercised while the connection stayed text-mode).
Verified end-to-end against the real embedded nym-client 1.1.76 binary,
with the exact wire format cross-checked against upstream nym source at
the pinned build commit.
Diffstat (limited to 'internal/nym/address.go')
| -rw-r--r-- | internal/nym/address.go | 131 |
1 files changed, 131 insertions, 0 deletions
diff --git a/internal/nym/address.go b/internal/nym/address.go new file mode 100644 index 0000000..f8c251a --- /dev/null +++ b/internal/nym/address.go @@ -0,0 +1,131 @@ +package nym + +import ( + "fmt" + "math/big" + "strings" +) + +const ( + pubkeySize = 32 + // RecipientLen is the wire length of a nym-client Recipient: identity || + // encryption || gateway pubkeys, 32 bytes each. + RecipientLen = 3 * pubkeySize +) + +var base58Alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + +// decodeBase58 decodes plain base58 (Bitcoin alphabet, no checksum), matching +// the Rust `bs58` crate defaults used by nym-sphinx for identity/encryption keys. +func decodeBase58(s string) ([]byte, error) { + if s == "" { + return nil, fmt.Errorf("empty base58 string") + } + + num := new(big.Int) + base := big.NewInt(58) + for _, r := range s { + idx := strings.IndexRune(base58Alphabet, r) + if idx < 0 { + return nil, fmt.Errorf("invalid base58 character %q", r) + } + num.Mul(num, base) + num.Add(num, big.NewInt(int64(idx))) + } + + decoded := num.Bytes() + + // Each leading '1' encodes a leading zero byte. + leadingZeros := 0 + for _, r := range s { + if r != '1' { + break + } + leadingZeros++ + } + + out := make([]byte, leadingZeros+len(decoded)) + copy(out[leadingZeros:], decoded) + return out, nil +} + +// ParseRecipient converts a nym address string "identity.encryption@gateway" +// into the 96-byte wire encoding expected by the nym-client binary WS protocol +// (identity_pubkey || encryption_pubkey || gateway_identity_pubkey). +func ParseRecipient(addr string) ([]byte, error) { + atParts := strings.Split(addr, "@") + if len(atParts) != 2 { + return nil, fmt.Errorf("nym address must contain exactly one '@': %q", addr) + } + clientHalf, gatewayHalf := atParts[0], atParts[1] + + dotParts := strings.Split(clientHalf, ".") + if len(dotParts) != 2 { + return nil, fmt.Errorf("nym address client part must contain exactly one '.': %q", addr) + } + + identity, err := decodeBase58(dotParts[0]) + if err != nil { + return nil, fmt.Errorf("decode identity key: %w", err) + } + encryption, err := decodeBase58(dotParts[1]) + if err != nil { + return nil, fmt.Errorf("decode encryption key: %w", err) + } + gateway, err := decodeBase58(gatewayHalf) + if err != nil { + return nil, fmt.Errorf("decode gateway key: %w", err) + } + + if len(identity) != pubkeySize || len(encryption) != pubkeySize || len(gateway) != pubkeySize { + return nil, fmt.Errorf("nym address %q: expected 3x%d byte keys, got %d/%d/%d", + addr, pubkeySize, len(identity), len(encryption), len(gateway)) + } + + out := make([]byte, 0, RecipientLen) + out = append(out, identity...) + out = append(out, encryption...) + out = append(out, gateway...) + return out, nil +} + +// FormatRecipient is the inverse of ParseRecipient: it encodes 96 raw +// recipient bytes back into "identity.encryption@gateway" base58 form. +func FormatRecipient(b []byte) (string, error) { + if len(b) != RecipientLen { + return "", fmt.Errorf("expected %d recipient bytes, got %d", RecipientLen, len(b)) + } + identity := encodeBase58(b[:pubkeySize]) + encryption := encodeBase58(b[pubkeySize : 2*pubkeySize]) + gateway := encodeBase58(b[2*pubkeySize:]) + return identity + "." + encryption + "@" + gateway, nil +} + +// encodeBase58 is the inverse of decodeBase58 (plain base58, Bitcoin alphabet, +// no checksum). +func encodeBase58(data []byte) string { + leadingZeros := 0 + for _, b := range data { + if b != 0 { + break + } + leadingZeros++ + } + + num := new(big.Int).SetBytes(data) + base := big.NewInt(58) + mod := new(big.Int) + var out []byte + for num.Sign() > 0 { + num.DivMod(num, base, mod) + out = append(out, base58Alphabet[mod.Int64()]) + } + for i := 0; i < leadingZeros; i++ { + out = append(out, '1') + } + // out was built least-significant-digit first; reverse it. + for i, j := 0, len(out)-1; i < j; i, j = i+1, j-1 { + out[i], out[j] = out[j], out[i] + } + return string(out) +} |
