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) }