Line data Source code
1 : import 'dart:convert';
2 : import 'dart:typed_data';
3 :
4 : import 'package:pointycastle/digests/sha256.dart';
5 : import 'package:walletkit_dart/src/domain/extensions.dart';
6 :
7 : /// Encode and decode bytes to base58 strings.
8 : ///
9 : /// An alphabet must be provided.
10 : ///
11 : /// In order to comply with Bitcoin and Ripple standard encoding Base58Check,
12 : /// use [Base58CheckCodec].
13 :
14 : const bitcoinAlphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
15 :
16 : const rippleAlphabet = 'rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz';
17 :
18 : const base58BitcoinCodec = Base58Codec(bitcoinAlphabet); // Bitcoin alphabet without checksum
19 :
20 : class Base58Codec extends Codec<List<int>, String> {
21 39 : const Base58Codec(this.alphabet);
22 : final String alphabet;
23 :
24 8 : @override
25 16 : Converter<List<int>, String> get encoder => Base58Encoder(alphabet);
26 :
27 3 : @override
28 6 : Converter<String, List<int>> get decoder => Base58Decoder(alphabet);
29 : }
30 :
31 : /// Encode bytes to a base58 string.
32 : class Base58Encoder extends Converter<List<int>, String> {
33 9 : const Base58Encoder(this.alphabet);
34 : final String alphabet;
35 :
36 9 : @override
37 : String convert(List<int> input) {
38 9 : if (input.isEmpty) return '';
39 :
40 : // copy bytes because we are going to change it
41 9 : input = Uint8List.fromList(input);
42 :
43 : // count number of leading zeros
44 : var leadingZeroes = 0;
45 36 : while (leadingZeroes < input.length && input[leadingZeroes] == 0) {
46 0 : leadingZeroes++;
47 : }
48 :
49 : var output = '';
50 : var startAt = leadingZeroes;
51 18 : while (startAt < input.length) {
52 9 : var mod = _divmod58(input, startAt);
53 27 : if (input[startAt] == 0) startAt++;
54 27 : output = alphabet[mod] + output;
55 : }
56 :
57 9 : if (output.isNotEmpty) {
58 36 : while (output[0] == alphabet[0]) {
59 0 : output = output.substring(1, output.length);
60 : }
61 : }
62 18 : while (leadingZeroes-- > 0) {
63 0 : output = alphabet[0] + output;
64 : }
65 :
66 : return output;
67 : }
68 :
69 : /// number -> number / 58
70 : /// returns number % 58
71 9 : static int _divmod58(List<int> number, int startAt) {
72 : var remaining = 0;
73 27 : for (var i = startAt; i < number.length; i++) {
74 36 : var num = (0xFF & remaining) * 256 + number[i];
75 18 : number[i] = num ~/ 58;
76 9 : remaining = num % 58;
77 : }
78 : return remaining;
79 : }
80 : }
81 :
82 : /// Decode base58 strings to bytes.
83 : class Base58Decoder extends Converter<String, List<int>> {
84 4 : const Base58Decoder(this.alphabet);
85 : final String alphabet;
86 :
87 4 : @override
88 : List<int> convert(String input) {
89 4 : if (input.isEmpty) return Uint8List(0);
90 :
91 : // generate base 58 index list from input string
92 8 : var input58 = List<int>.filled(input.length, 0);
93 12 : for (var i = 0; i < input.length; i++) {
94 12 : var charint = alphabet.indexOf(input[i]);
95 4 : if (charint < 0) {
96 0 : throw FormatException('Invalid input formatting for Base58 decoding.');
97 : }
98 4 : input58[i] = charint;
99 : }
100 :
101 : // count leading zeroes
102 : var leadingZeroes = 0;
103 16 : while (leadingZeroes < input58.length && input58[leadingZeroes] == 0) {
104 0 : leadingZeroes++;
105 : }
106 :
107 : // decode
108 8 : var output = Uint8List(input.length);
109 4 : var j = output.length;
110 : var startAt = leadingZeroes;
111 8 : while (startAt < input58.length) {
112 4 : var mod = _divmod256(input58, startAt);
113 12 : if (input58[startAt] == 0) startAt++;
114 8 : output[--j] = mod;
115 : }
116 :
117 : // remove unnecessary leading zeroes
118 16 : while (j < output.length && output[j] == 0) {
119 4 : j++;
120 : }
121 8 : return output.sublist(j - leadingZeroes);
122 : }
123 :
124 : /// number -> number / 256
125 : /// returns number % 256
126 4 : static int _divmod256(List<int> number58, int startAt) {
127 : var remaining = 0;
128 12 : for (var i = startAt; i < number58.length; i++) {
129 16 : var num = 58 * remaining + (number58[i] & 0xFF);
130 8 : number58[i] = num ~/ 256;
131 4 : remaining = num % 256;
132 : }
133 : return remaining;
134 : }
135 : }
136 :
137 : class Base58CheckPayload {
138 2 : const Base58CheckPayload(this.version, this.payload);
139 : final int version;
140 : final List<int> payload;
141 0 : @override
142 : bool operator ==(Object other) =>
143 0 : other is Base58CheckPayload && version == other.version && _areEqual(payload, other.payload);
144 0 : @override
145 0 : int get hashCode => version.hashCode ^ hash(payload);
146 : }
147 :
148 : /// A codec for Base58Check, a binary-to-string encoding used
149 : /// in cryptocurrencies like Bitcoin and Ripple.
150 : ///
151 : /// The constructor requires the alphabet and a function that
152 : /// performs a SINGLE-round SHA-256 digest on a [List<int>] and
153 : /// returns a [List<int>] as result.
154 : ///
155 : /// For all details about Base58Check, see the Bitcoin wiki page:
156 : /// https://en.bitcoin.it/wiki/Base58Check_encoding
157 : class Base58CheckCodec extends Codec<Base58CheckPayload, String> {
158 2 : Base58CheckCodec(this.alphabet)
159 2 : : _encoder = Base58CheckEncoder(alphabet),
160 2 : _decoder = Base58CheckDecoder(alphabet);
161 :
162 : /// A codec that works with the Ripple alphabet and the SHA256 hash function.
163 0 : Base58CheckCodec.ripple() : this(rippleAlphabet);
164 :
165 : /// A codec that works with the Bitcoin alphabet and the SHA256 hash function.
166 4 : Base58CheckCodec.bitcoin() : this(bitcoinAlphabet);
167 :
168 : static const bitcoinAlphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
169 :
170 : static const rippleAlphabet = 'rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz';
171 :
172 : final String alphabet;
173 :
174 : Base58CheckEncoder _encoder;
175 : Base58CheckDecoder _decoder;
176 :
177 1 : @override
178 1 : Converter<Base58CheckPayload, String> get encoder => _encoder;
179 :
180 2 : @override
181 2 : Converter<String, Base58CheckPayload> get decoder => _decoder;
182 :
183 0 : Base58CheckPayload decodeUnchecked(String encoded) => _decoder.convertUnchecked(encoded);
184 : }
185 :
186 : class Base58CheckEncoder extends Converter<Base58CheckPayload, String> {
187 2 : const Base58CheckEncoder(this.alphabet);
188 : final String alphabet;
189 :
190 1 : @override
191 : String convert(Base58CheckPayload input) {
192 5 : var bytes = Uint8List(input.payload.length + 1 + 4);
193 3 : bytes[0] = 0xFF & input.version;
194 4 : bytes.setRange(1, bytes.length - 4, input.payload);
195 4 : var checksum = _hash(bytes.sublist(0, bytes.length - 4));
196 5 : bytes.setRange(bytes.length - 4, bytes.length, checksum.getRange(0, 4));
197 3 : return Base58Encoder(alphabet).convert(bytes);
198 : }
199 : }
200 :
201 2 : List<int> _hash(List<int> b) =>
202 10 : SHA256Digest().process(SHA256Digest().process(Uint8List.fromList(b)));
203 :
204 : class Base58CheckDecoder extends Converter<String, Base58CheckPayload> {
205 2 : const Base58CheckDecoder(this.alphabet);
206 : final String alphabet;
207 :
208 2 : @override
209 2 : Base58CheckPayload convert(String input) => _convert(input, true);
210 :
211 0 : Base58CheckPayload convertUnchecked(String encoded) => _convert(encoded, false);
212 :
213 2 : Base58CheckPayload _convert(String encoded, bool validateChecksum) {
214 6 : var bytes = Base58Decoder(alphabet).convert(encoded);
215 4 : if (bytes.length < 5) {
216 0 : throw FormatException('Invalid Base58Check encoded string: must be at least size 5');
217 : }
218 8 : var checksum = _hash(bytes.sublist(0, bytes.length - 4));
219 8 : var providedChecksum = bytes.sublist(bytes.length - 4, bytes.length);
220 4 : if (validateChecksum && !_areEqual(providedChecksum, checksum.sublist(0, 4))) {
221 1 : throw FormatException('Invalid checksum in Base58Check encoding.');
222 : }
223 10 : return Base58CheckPayload(bytes[0], bytes.sublist(1, bytes.length - 4));
224 : }
225 : }
226 :
227 2 : bool _areEqual(List<int> left, List<int> right) {
228 : if (identical(left, right)) {
229 : return true;
230 : }
231 :
232 6 : if (left.length != right.length) {
233 : return false;
234 : }
235 :
236 6 : for (var i = 0; i < left.length; i++) {
237 6 : if (left[i] != right[i]) {
238 : return false;
239 : }
240 : }
241 :
242 : return true;
243 : }
244 :
245 0 : int hash(List<int>? list) {
246 : const hashMask = 0x7fffffff;
247 :
248 0 : if (list == null) return null.hashCode;
249 : // Jenkins's one-at-a-time hash function.
250 : // This code is almost identical to the one in IterableEquality, except
251 : // that it uses indexing instead of iterating to get the elements.
252 : var hash = 0;
253 0 : for (var i = 0; i < list.length; i++) {
254 0 : var c = list[i].hashCode;
255 0 : hash = (hash + c) & hashMask;
256 0 : hash = (hash + (hash << 10)) & hashMask;
257 0 : hash ^= (hash >> 6);
258 : }
259 0 : hash = (hash + (hash << 3)) & hashMask;
260 0 : hash ^= (hash >> 11);
261 0 : hash = (hash + (hash << 15)) & hashMask;
262 : return hash;
263 : }
264 :
265 1 : String base58CheckEncode(int version, Uint8List payload) {
266 2 : return Base58CheckCodec.bitcoin().encode(
267 1 : Base58CheckPayload(version, payload),
268 : );
269 : }
270 :
271 2 : Uint8List base58CheckDecode(String input) {
272 8 : return Uint8List.fromList(Base58CheckCodec.bitcoin().decode(input).payload);
273 : }
274 :
275 1 : Uint8List base58CheckDecodeWithVersion(String input) {
276 2 : final result = Base58CheckCodec.bitcoin().decode(input);
277 4 : return Uint8List.fromList([result.version, ...result.payload]);
278 : }
279 :
280 3 : Uint8List base58Decode(String input, {bool withChecksum = false}) {
281 9 : final decoded = base58BitcoinCodec.decoder.convert(input).toUint8List;
282 : if (withChecksum) return decoded;
283 9 : return decoded.sublist(0, decoded.length - 4);
284 : }
285 :
286 8 : String base58Encode(Uint8List input) {
287 16 : return base58BitcoinCodec.encoder.convert(input);
288 : }
|