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