summaryrefslogtreecommitdiffstats
path: root/internal/nym/address.go
blob: f8c251add261eeebe9dda504724e07025f4f17c6 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
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)
}