├── .circleci └── config.yml ├── .editorconfig ├── .gitignore ├── CHANGELOG.md ├── COPYING ├── README.md ├── analysis_options.yaml ├── example └── main.dart ├── lib ├── bitcoin_bip32.dart └── src │ ├── chain.dart │ ├── crypto.dart │ └── exceptions.dart ├── pubspec.yaml └── test └── bitcoin_bip32_test.dart /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.0 2 | jobs: 3 | build: 4 | docker: 5 | - image: cirrusci/flutter 6 | steps: 7 | - checkout 8 | - run: flutter doctor -v 9 | - run: pub get 10 | - run: pub run test 11 | - run: flutter format . --set-exit-if-changed 12 | - run: dartanalyzer . --options=analysis_options.yaml --fatal-hints 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | # Unix-style newlines with a newline ending every file 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | # 4 space indentation 8 | [*.dart] 9 | indent_style = space 10 | indent_size = 2 11 | 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dart_tool/ 2 | .packages 3 | build/ 4 | 5 | // Except for application packages 6 | pubspec.lock 7 | 8 | doc/api/ 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | v0.1.1 2 | * Clarify serialized key length 3 | 4 | v0.1.0 5 | * Initial release 6 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright 2020 Harm Aarts 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BIP32 2 | 3 | [![pub package](https://img.shields.io/pub/v/bitcoin_bip32.svg)](https://pub.dartlang.org/packages/bitcoin_bip32) 4 | [![CircleCI](https://circleci.com/gh/inapay/bitcoin_bip32.svg?style=svg)](https://circleci.com/gh/inapay/bitcoin_bip32) 5 | 6 | An implementation of the [BIP32 spec] for Hierarchical Deterministic Bitcoin 7 | addresses. No [superimposing wallet structure] has been defined. 8 | 9 | ## Examples 10 | 11 | You can use this library in two ways; one with a serialized public or private 12 | HD key or with a hex encoded seed. 13 | 14 | Look at the tests to see more elaborate uses. 15 | 16 | ### With a seed 17 | 18 | ``` 19 | Chain chain = Chain.seed(hex.encode(utf8.encode("some seed"))); 20 | ExtendedPrivateKey key = chain.forPath("m/0/100"); 21 | print(key); 22 | // => xprv9ww7sMFLzJN5LhdyGB9zfhm9MAVZ8P97iTWQtVeAg2rA9MPZfJUESWe6NaSu44zz44QBjWtwH9HNfJ4vFiUwfrTCvf7AGrgYpXe17bfh2Je 23 | ``` 24 | 25 | The `key` has a field called `key` which contains a `BigInt`. This is the actual 26 | key. 27 | 28 | ### Importing a HD private key 29 | 30 | ``` 31 | Chain chain = Chain.import("xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi"); 32 | ExtendedPrivateKey childKey = chain.forPath("m/0/100"); 33 | ``` 34 | 35 | ### Importing a HD public key 36 | 37 | ``` 38 | Chain chain = Chain.import("xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8"); 39 | ExtendedPublic childKey = chain.forPath("M/0/100"); 40 | ``` 41 | 42 | The `key` has a field called `q` which contains a [`ECPoint`]. This is the actual 43 | key. 44 | 45 | Please note that trying to generate a private key from a public key will throw 46 | an exception. 47 | 48 | 49 | ## Exceptions 50 | 51 | There is a tiny chance a child key derivation fails. Please catch the 52 | appropriate exceptions in your code. 53 | 54 | These exceptions are: 55 | - `KeyZero` 56 | - `KeyBiggerThanOrder` 57 | - `KeyInfinite` 58 | 59 | ## Installing 60 | 61 | Add it to your `pubspec.yaml`: 62 | 63 | ``` 64 | dependencies: 65 | bip32: ^0.1.0 66 | ``` 67 | 68 | ## Licence overview 69 | 70 | All files in this repository fall under the license specified in 71 | [COPYING](COPYING). The project is licensed as [AGPL with a lesser 72 | clause](https://www.gnu.org/licenses/agpl-3.0.en.html). It may be used within a 73 | proprietary project, but the core library and any changes to it must be 74 | published online. Source code for this library must always remain free for 75 | everybody to access. 76 | 77 | ## Thanks 78 | 79 | Without the guiding code of [go-bip32] and [money-tree] projects this library would have been a significantly bigger struggle. 80 | 81 | 82 | [BIP32 spec]: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki 83 | [superimposing wallet structure]: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#specification-wallet-structure 84 | [go-bip32]: https://github.com/tyler-smith/go-bip32/ 85 | [money-tree]: https://github.com/GemHQ/money-tree/ 86 | [`ECPoint`]: https://pub.dartlang.org/documentation/pointycastle/1.0.0-rc3/pointycastle.api.ecc/ECPoint-class.html 87 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:pedantic/analysis_options.1.9.0.yaml 2 | linter: 3 | rules: 4 | - always_put_required_named_parameters_first 5 | - always_require_non_null_named_parameters 6 | - avoid_annotating_with_dynamic 7 | - avoid_bool_literals_in_conditional_expressions 8 | - avoid_catching_errors 9 | - avoid_classes_with_only_static_members 10 | - avoid_empty_else 11 | - avoid_init_to_null 12 | - avoid_null_checks_in_equality_operators 13 | - avoid_print 14 | - avoid_relative_lib_imports 15 | - avoid_return_types_on_setters 16 | - avoid_returning_null 17 | - avoid_returning_null_for_future 18 | - avoid_returning_null_for_void 19 | - avoid_shadowing_type_parameters 20 | - avoid_single_cascade_in_expression_statements 21 | - avoid_types_as_parameter_names 22 | - avoid_types_on_closure_parameters 23 | - avoid_void_async 24 | - await_only_futures 25 | - camel_case_types 26 | - cancel_subscriptions 27 | - cascade_invocations 28 | - close_sinks 29 | - comment_references 30 | - constant_identifier_names 31 | - control_flow_in_finally 32 | - curly_braces_in_flow_control_structures 33 | - directives_ordering 34 | - empty_catches 35 | - empty_constructor_bodies 36 | - empty_statements 37 | - file_names 38 | - hash_and_equals 39 | - implementation_imports 40 | - invariant_booleans 41 | - iterable_contains_unrelated_type 42 | - join_return_with_assignment 43 | - library_names 44 | - library_prefixes 45 | - list_remove_unrelated_type 46 | - no_duplicate_case_values 47 | - non_constant_identifier_names 48 | - null_closures 49 | - only_throw_errors 50 | - overridden_fields 51 | - package_api_docs 52 | - package_names 53 | - package_prefixed_library_names 54 | - prefer_collection_literals 55 | - prefer_conditional_assignment 56 | - prefer_const_declarations 57 | - prefer_contains 58 | - prefer_equal_for_default_values 59 | - prefer_final_fields 60 | - prefer_for_elements_to_map_fromIterable 61 | - prefer_foreach 62 | - prefer_function_declarations_over_variables 63 | - prefer_if_elements_to_conditional_expressions 64 | - prefer_if_null_operators 65 | - prefer_initializing_formals 66 | - prefer_inlined_adds 67 | - prefer_int_literals 68 | - prefer_interpolation_to_compose_strings 69 | - prefer_is_empty 70 | - prefer_is_not_empty 71 | - prefer_iterable_whereType 72 | - prefer_null_aware_operators 73 | - prefer_void_to_null 74 | - provide_deprecation_message 75 | - recursive_getters 76 | - slash_for_doc_comments 77 | - sort_child_properties_last 78 | - sort_constructors_first 79 | - sort_pub_dependencies 80 | - sort_unnamed_constructors_first 81 | - test_types_in_equals 82 | - throw_in_finally 83 | - type_init_formals 84 | - unawaited_futures 85 | - unnecessary_await_in_return 86 | - unnecessary_brace_in_string_interps 87 | - unnecessary_const 88 | - unnecessary_getters_setters 89 | - unnecessary_lambdas 90 | - unnecessary_new 91 | - unnecessary_null_aware_assignments 92 | - unnecessary_null_in_if_null_operators 93 | - unnecessary_overrides 94 | - unnecessary_parenthesis 95 | - unnecessary_statements 96 | - unnecessary_this 97 | - unrelated_type_equality_checks 98 | - unsafe_html 99 | - use_full_hex_values_for_flutter_colors 100 | - use_function_type_syntax_for_parameters 101 | - use_rethrow_when_possible 102 | - use_setters_to_change_properties 103 | - use_string_buffers 104 | - use_to_and_as_if_applicable 105 | - valid_regexps 106 | - void_checks 107 | -------------------------------------------------------------------------------- /example/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:convert/convert.dart'; 3 | import 'package:bitcoin_bip32/bitcoin_bip32.dart'; 4 | 5 | //ignore_for_file: avoid_print 6 | 7 | void main() { 8 | var chain = Chain.seed(hex.encode(utf8.encode('some seed'))); 9 | var key = chain.forPath('m/0/100'); 10 | print(key); 11 | } 12 | -------------------------------------------------------------------------------- /lib/bitcoin_bip32.dart: -------------------------------------------------------------------------------- 1 | export 'src/chain.dart'; 2 | export 'src/crypto.dart'; 3 | export 'src/exceptions.dart'; 4 | -------------------------------------------------------------------------------- /lib/src/chain.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:convert/convert.dart'; 4 | 5 | import 'crypto.dart'; 6 | import 'exceptions.dart'; 7 | 8 | /// Use this class to generate extended keys. You can create an instance of 9 | /// this class with either a serialized extended key ([Chain.import]) or a 10 | /// hex encoded master seed ([Chain.seed]). 11 | class Chain { 12 | /// Create a chain based on a hex seed. 13 | Chain.seed(String seed) { 14 | Uint8List seedBytes = hex.decoder.convert(seed); 15 | root = ExtendedPrivateKey.master(seedBytes); 16 | } 17 | 18 | /// Create a chain based on a serialized private or public key. 19 | Chain.import(String key) { 20 | root = ExtendedKey.deserialize(key); 21 | } 22 | 23 | static const String _hardenedSuffix = "'"; 24 | static const String _privateKeyPrefix = 'm'; 25 | static const String _publicKeyPrefix = 'M'; 26 | 27 | /// The root out of which all keys can be derived. 28 | ExtendedKey root; 29 | 30 | bool get isPrivate => root is ExtendedPrivateKey; 31 | 32 | /// Derives a key based on a path. 33 | /// 34 | /// A path is a slash delimited string starting with 'm' for private key and 35 | /// 'M' for a public key. Hardened keys are indexed with a tick. 36 | /// Example: "m/100/1'". 37 | /// This is the first Hardened private extended key on depth 2. 38 | ExtendedKey forPath(String path) { 39 | _validatePath(path); 40 | 41 | var wantsPrivate = path[0] == _privateKeyPrefix; 42 | var children = _parseChildren(path); 43 | 44 | if (children.isEmpty) { 45 | if (wantsPrivate) { 46 | return root; 47 | } 48 | return root.publicKey(); 49 | } 50 | 51 | dynamic derivationFunction = wantsPrivate 52 | ? deriveExtendedPrivateChildKey 53 | : deriveExtendedPublicChildKey; 54 | 55 | return children.fold(root, (previousKey, childNumber) { 56 | return derivationFunction(previousKey, childNumber); 57 | }); 58 | } 59 | 60 | void _validatePath(String path) { 61 | var kind = path.split('/').removeAt(0); 62 | 63 | if (![_privateKeyPrefix, _publicKeyPrefix].contains(kind)) { 64 | throw InvalidPath("Path needs to start with 'm' or 'M'"); 65 | } 66 | 67 | if (kind == _privateKeyPrefix && root is ExtendedPublicKey) { 68 | throw InvalidPath('Cannot derive private key from public master'); 69 | } 70 | } 71 | 72 | Iterable _parseChildren(String path) { 73 | var explodedList = path.split('/') 74 | ..removeAt(0) 75 | ..removeWhere((child) => child == ''); 76 | 77 | return explodedList.map((pathFragment) { 78 | if (pathFragment.endsWith(_hardenedSuffix)) { 79 | pathFragment = pathFragment.substring(0, pathFragment.length - 1); 80 | return int.parse(pathFragment) + firstHardenedChild; 81 | } else { 82 | return int.parse(pathFragment); 83 | } 84 | }); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/src/crypto.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:base58check/base58.dart'; 5 | import 'package:convert/convert.dart'; 6 | import 'package:pointycastle/api.dart'; 7 | import 'package:pointycastle/macs/hmac.dart'; 8 | import 'package:pointycastle/digests/sha256.dart'; 9 | import 'package:pointycastle/digests/sha512.dart'; 10 | import 'package:pointycastle/digests/ripemd160.dart'; 11 | import 'package:pointycastle/ecc/curves/secp256k1.dart'; 12 | import 'package:pointycastle/ecc/api.dart'; 13 | // ignore: implementation_imports 14 | import 'package:pointycastle/src/utils.dart' as utils; 15 | 16 | import 'exceptions.dart'; 17 | 18 | final sha256digest = SHA256Digest(); 19 | final sha512digest = SHA512Digest(); 20 | final ripemd160digest = RIPEMD160Digest(); 21 | 22 | /// The Bitcoin curve 23 | final curve = ECCurve_secp256k1(); 24 | 25 | /// Used for the Base58 encoding. 26 | const String alphabet = 27 | '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; 28 | 29 | /// From the specification (in bytes): 30 | /// 4 version 31 | /// 1 depth 32 | /// 4 fingerprint 33 | /// 4 child number 34 | /// 32 chain code 35 | /// 33 public or private key 36 | const int lengthOfSerializedKey = 78; 37 | 38 | /// Length of checksum in bytes 39 | const int lengthOfChecksum = 4; 40 | 41 | /// From the specification the length of a private of public key 42 | const int lengthOfKey = 33; 43 | 44 | /// FirstHardenedChild is the index of the firxt "hardened" child key as per the 45 | /// bip32 spec 46 | const int firstHardenedChild = 0x80000000; 47 | 48 | /// The 4 version bytes for the private key serialization as defined in the 49 | /// BIP21 spec 50 | final Uint8List privateKeyVersion = hex.decode('0488ADE4'); 51 | 52 | /// The 4 version bytes for the public key serialization as defined in the 53 | /// BIP21 spec 54 | final Uint8List publicKeyVersion = hex.decode('0488B21E'); 55 | 56 | /// From the BIP32 spec. Used when calculating the hmac of the seed 57 | final Uint8List masterKey = utf8.encoder.convert('Bitcoin seed'); 58 | 59 | /// AKA 'point(k)' in the specification 60 | ECPoint publicKeyFor(BigInt d) { 61 | return ECPublicKey(curve.G * d, curve).Q; 62 | } 63 | 64 | /// AKA 'ser_P(P)' in the specification 65 | Uint8List compressed(ECPoint q) { 66 | return q.getEncoded(true); 67 | } 68 | 69 | /// AKA 'ser_32(i)' in the specification 70 | Uint8List serializeTo4bytes(int i) { 71 | var bytes = ByteData(4)..setInt32(0, i, Endian.big); 72 | 73 | return bytes.buffer.asUint8List(); 74 | } 75 | 76 | /// CKDpriv in the specficiation 77 | ExtendedPrivateKey deriveExtendedPrivateChildKey( 78 | ExtendedPrivateKey parent, int childNumber) { 79 | var message = childNumber >= firstHardenedChild 80 | ? _derivePrivateMessage(parent, childNumber) 81 | : _derivePublicMessage(parent.publicKey(), childNumber); 82 | var hash = hmacSha512(parent.chainCode, message); 83 | 84 | var leftSide = utils.decodeBigInt(_leftFrom(hash)); 85 | if (leftSide >= curve.n) { 86 | throw KeyBiggerThanOrder(); 87 | } 88 | 89 | var childPrivateKey = (leftSide + parent.key) % curve.n; 90 | if (childPrivateKey == BigInt.zero) { 91 | throw KeyZero(); 92 | } 93 | 94 | var chainCode = _rightFrom(hash); 95 | 96 | return ExtendedPrivateKey( 97 | key: childPrivateKey, 98 | chainCode: chainCode, 99 | childNumber: childNumber, 100 | depth: parent.depth + 1, 101 | parentFingerprint: parent.fingerprint, 102 | ); 103 | } 104 | 105 | /// CKDpub in the specification 106 | ExtendedPublicKey deriveExtendedPublicChildKey( 107 | ExtendedPublicKey parent, int childNumber) { 108 | if (childNumber >= firstHardenedChild) { 109 | throw InvalidChildNumber(); 110 | } 111 | 112 | var message = _derivePublicMessage(parent, childNumber); 113 | var hash = hmacSha512(parent.chainCode, message); 114 | 115 | var leftSide = utils.decodeBigInt(_leftFrom(hash)); 116 | if (leftSide >= curve.n) { 117 | throw KeyBiggerThanOrder(); 118 | } 119 | 120 | var childPublicKey = publicKeyFor(leftSide) + parent.q; 121 | if (childPublicKey.isInfinity) { 122 | throw KeyInfinite(); 123 | } 124 | 125 | return ExtendedPublicKey( 126 | q: childPublicKey, 127 | chainCode: _rightFrom(hash), 128 | childNumber: childNumber, 129 | depth: parent.depth + 1, 130 | parentFingerprint: parent.fingerprint, 131 | ); 132 | } 133 | 134 | Uint8List _paddedEncodedBigInt(BigInt i) { 135 | var fullLength = Uint8List(lengthOfKey - 1); 136 | var encodedBigInt = utils.encodeBigInt(i); 137 | fullLength.setAll(fullLength.length - encodedBigInt.length, encodedBigInt); 138 | 139 | return fullLength; 140 | } 141 | 142 | Uint8List _derivePrivateMessage(ExtendedPrivateKey key, int childNumber) { 143 | var message = Uint8List(37) 144 | ..setAll(1, _paddedEncodedBigInt(key.key)) 145 | ..setAll(33, serializeTo4bytes(childNumber)); 146 | 147 | return message; 148 | } 149 | 150 | Uint8List _derivePublicMessage(ExtendedPublicKey key, int childNumber) { 151 | var message = Uint8List(37) 152 | ..setAll(0, compressed(key.q)) 153 | ..setAll(33, serializeTo4bytes(childNumber)); 154 | 155 | return message; 156 | } 157 | 158 | /// This function returns a list of length 64. The first half is the key, the 159 | /// second half is the chain code. 160 | Uint8List hmacSha512(Uint8List key, Uint8List message) { 161 | var hmac = HMac(sha512digest, 128)..init(KeyParameter(key)); 162 | return hmac.process(message); 163 | } 164 | 165 | /// Double hash the data: RIPEMD160(SHA256(data)) 166 | Uint8List hash160(Uint8List data) { 167 | return ripemd160digest.process(sha256digest.process(data)); 168 | } 169 | 170 | Uint8List _leftFrom(Uint8List list) { 171 | return list.sublist(0, 32); 172 | } 173 | 174 | Uint8List _rightFrom(Uint8List list) { 175 | return list.sublist(32); 176 | } 177 | 178 | // NOTE wow, this is annoying 179 | bool equal(Iterable a, Iterable b) { 180 | if (a.length != b.length) { 181 | return false; 182 | } 183 | 184 | for (var i = 0; i < a.length; i++) { 185 | if (a.elementAt(i) != b.elementAt(i)) { 186 | return false; 187 | } 188 | } 189 | 190 | return true; 191 | } 192 | 193 | // NOTE yikes, what a dance, surely I'm overlooking something 194 | Uint8List sublist(Uint8List list, int start, int end) { 195 | return Uint8List.fromList(list.getRange(start, end).toList()); 196 | } 197 | 198 | /// Abstract class on which [ExtendedPrivateKey] and [ExtendedPublicKey] are based. 199 | abstract class ExtendedKey { 200 | ExtendedKey({ 201 | this.version, 202 | this.depth, 203 | this.childNumber, 204 | this.chainCode, 205 | this.parentFingerprint, 206 | }); 207 | 208 | /// Take a HD key serialized according to the spec and deserialize it. 209 | /// 210 | /// Works for both private and public keys. 211 | factory ExtendedKey.deserialize(String key) { 212 | var decodedKey = Base58Codec(alphabet).decode(key); 213 | if (decodedKey.length != lengthOfSerializedKey + lengthOfChecksum) { 214 | throw InvalidKeyLength( 215 | decodedKey.length, lengthOfSerializedKey + lengthOfChecksum); 216 | } 217 | 218 | if (equal(decodedKey.getRange(0, 4), privateKeyVersion)) { 219 | return ExtendedPrivateKey.deserialize(decodedKey); 220 | } 221 | 222 | return ExtendedPublicKey.deserialize(decodedKey); 223 | } 224 | 225 | /// 32 bytes 226 | Uint8List chainCode; 227 | 228 | int childNumber; 229 | 230 | int depth; 231 | 232 | /// 4 bytes 233 | final Uint8List version; 234 | 235 | /// 4 bytes 236 | Uint8List parentFingerprint; 237 | 238 | /// Returns the first 4 bytes of the hash160 compressed public key. 239 | Uint8List get fingerprint; 240 | 241 | /// Returns the public key assocated with the extended key. 242 | /// 243 | /// In case of [ExtendedPublicKey] returns self. 244 | ExtendedPublicKey publicKey(); 245 | 246 | List _serialize() { 247 | return [ 248 | ...version, 249 | depth, 250 | ...parentFingerprint, 251 | ...serializeTo4bytes(childNumber), 252 | ...chainCode, 253 | ..._serializedKey() 254 | ]; 255 | } 256 | 257 | List _serializedKey(); 258 | 259 | /// Used to verify deserialized keys. 260 | bool verifyChecksum(Uint8List externalChecksum) { 261 | return equal(_checksum(), externalChecksum.toList()); 262 | } 263 | 264 | Iterable _checksum() { 265 | return sha256digest 266 | .process(sha256digest.process(Uint8List.fromList(_serialize()))) 267 | .getRange(0, 4); 268 | } 269 | 270 | /// Returns the string representation of this extended key. This can be 271 | /// written to disk for future deserializion. 272 | @override 273 | String toString() { 274 | var payload = _serialize()..addAll(_checksum()); 275 | 276 | return Base58Codec(alphabet).encode(payload); 277 | } 278 | } 279 | 280 | /// An extended private key as defined by the BIP32 specification. 281 | /// 282 | /// In the lingo of the spec this is a `(k, c)`. 283 | /// This can be used to generate a extended public key or further child keys. 284 | /// Note that the spec talks about a 'neutered' key, this is the public key 285 | /// associated with a private key. 286 | class ExtendedPrivateKey extends ExtendedKey { 287 | ExtendedPrivateKey({ 288 | this.key, 289 | int depth, 290 | int childNumber, 291 | Uint8List chainCode, 292 | Uint8List parentFingerprint, 293 | }) : super( 294 | version: privateKeyVersion, 295 | depth: depth, 296 | childNumber: childNumber, 297 | parentFingerprint: parentFingerprint, 298 | chainCode: chainCode); 299 | 300 | ExtendedPrivateKey.master(Uint8List seed) 301 | : super(version: privateKeyVersion) { 302 | var hash = hmacSha512(masterKey, seed); 303 | key = utils.decodeBigInt(_leftFrom(hash)); 304 | chainCode = _rightFrom(hash); 305 | depth = 0; 306 | childNumber = 0; 307 | parentFingerprint = Uint8List.fromList([0, 0, 0, 0]); 308 | } 309 | 310 | factory ExtendedPrivateKey.deserialize(Uint8List key) { 311 | var extendedPrivateKey = ExtendedPrivateKey( 312 | depth: key[4], 313 | parentFingerprint: sublist(key, 5, 9), 314 | childNumber: ByteData.view(sublist(key, 9, 13).buffer).getInt32(0), 315 | chainCode: sublist(key, 13, 45), 316 | key: utils.decodeBigInt(sublist(key, 46, 78)), 317 | ); 318 | 319 | if (!extendedPrivateKey.verifyChecksum(sublist(key, lengthOfSerializedKey, 320 | lengthOfSerializedKey + lengthOfChecksum))) { 321 | throw InvalidChecksum(); 322 | } 323 | 324 | return extendedPrivateKey; 325 | } 326 | 327 | BigInt key; 328 | 329 | @override 330 | ExtendedPublicKey publicKey() { 331 | return ExtendedPublicKey( 332 | q: publicKeyFor(key), 333 | depth: depth, 334 | childNumber: childNumber, 335 | chainCode: chainCode, 336 | parentFingerprint: parentFingerprint, 337 | ); 338 | } 339 | 340 | @override 341 | Uint8List get fingerprint => publicKey().fingerprint; 342 | 343 | @override 344 | List _serializedKey() { 345 | var serialization = Uint8List(lengthOfKey); 346 | serialization[0] = 0; 347 | var encodedKey = _paddedEncodedBigInt(key); 348 | serialization.setAll(1, encodedKey); 349 | 350 | return serialization.toList(); 351 | } 352 | } 353 | 354 | /// An extended public key as defined by the BIP32 specification. 355 | /// 356 | /// In the lingo of the spec this is a `(K, c)`. 357 | /// This can be used to generate further public child keys only. 358 | class ExtendedPublicKey extends ExtendedKey { 359 | ExtendedPublicKey({ 360 | this.q, 361 | depth, 362 | childNumber, 363 | chainCode, 364 | parentFingerprint, 365 | }) : super( 366 | version: publicKeyVersion, 367 | depth: depth, 368 | childNumber: childNumber, 369 | parentFingerprint: parentFingerprint, 370 | chainCode: chainCode); 371 | 372 | factory ExtendedPublicKey.deserialize(Uint8List key) { 373 | var extendedPublickey = ExtendedPublicKey( 374 | depth: key[4], 375 | parentFingerprint: sublist(key, 5, 9), 376 | childNumber: ByteData.view(sublist(key, 9, 13).buffer).getInt32(0), 377 | chainCode: sublist(key, 13, 45), 378 | q: _decodeCompressedECPoint(sublist(key, 45, 78)), 379 | ); 380 | 381 | if (!extendedPublickey.verifyChecksum(sublist(key, 78, 82))) { 382 | throw InvalidChecksum(); 383 | } 384 | 385 | return extendedPublickey; 386 | } 387 | 388 | ECPoint q; 389 | 390 | @override 391 | Uint8List get fingerprint { 392 | var identifier = hash160(compressed(q)); 393 | return Uint8List.view(identifier.buffer, 0, 4); 394 | } 395 | 396 | @override 397 | ExtendedPublicKey publicKey() { 398 | return this; 399 | } 400 | 401 | @override 402 | List _serializedKey() { 403 | return compressed(q).toList(); 404 | } 405 | 406 | static ECPoint _decodeCompressedECPoint(Uint8List encodedPoint) { 407 | return curve.curve.decodePoint(encodedPoint.toList()); 408 | } 409 | } 410 | 411 | //ignore_for_file: avoid_print 412 | void debug(List payload) { 413 | print('version: ${payload.getRange(0, 4)}'); 414 | print('depth: ${payload.getRange(4, 5)}'); 415 | print('parent fingerprint: ${payload.getRange(5, 9)}'); 416 | print('childNumber: ${payload.getRange(9, 13)}'); 417 | print('chaincode: ${payload.getRange(13, 46)}'); 418 | print('key: ${payload.getRange(46, 78)}'); 419 | print('checksum: ${payload.getRange(78, 82)}'); 420 | } 421 | -------------------------------------------------------------------------------- /lib/src/exceptions.dart: -------------------------------------------------------------------------------- 1 | /// Thrown when deserializing a key with an invalid checksum. 2 | /// 3 | /// The serialization contains a checksum, if the calculated checksum doesn't 4 | /// match the stored checksum this exception is thrown. 5 | class InvalidChecksum implements Exception { 6 | @override 7 | String toString() => 'Checksum verification failed'; 8 | } 9 | 10 | /// Thrown when a derived private key is zero. 11 | /// 12 | /// Retry with an other child number. 13 | class KeyZero implements Exception { 14 | @override 15 | String toString() => 'Key is zero'; 16 | } 17 | 18 | /// Thrown when a derived key is bigger than the order of the curve. 19 | /// 20 | /// Retry with an other child number. 21 | class KeyBiggerThanOrder implements Exception { 22 | @override 23 | String toString() => 'Integer is bigger than order of curve'; 24 | } 25 | 26 | /// Thrown when trying to derive a public key with a hardened child number. 27 | class InvalidChildNumber implements Exception { 28 | @override 29 | String toString() => 'Child number is bigger than hardened child number'; 30 | } 31 | 32 | /// Thrown when a derived public key is infinite. 33 | /// 34 | /// Retry with an other child number. 35 | class KeyInfinite implements Exception { 36 | @override 37 | String toString() => 'ECPoint is infinite'; 38 | } 39 | 40 | /// Thrown when trying to derive a child key with a wrong path. 41 | class InvalidPath implements Exception { 42 | InvalidPath(this.message); 43 | 44 | String message; 45 | 46 | @override 47 | String toString() => message; 48 | } 49 | 50 | /// Thrown when deserializing a key which is not of correct length. 51 | class InvalidKeyLength implements Exception { 52 | InvalidKeyLength(this.actual, this.expected); 53 | 54 | int actual; 55 | int expected; 56 | 57 | @override 58 | String toString() => 59 | 'Key length not correct. Should be $expected, is $actual'; 60 | } 61 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: bitcoin_bip32 2 | version: 0.1.2 3 | description: Library implementing Bitcoins BIP32 (Hierarchical Deterministic key derivation) specification in a Flutter friendly fashion. 4 | author: Harm Aarts 5 | homepage: https://github.com/haarts/bitcoin_bip32 6 | 7 | dependencies: 8 | pointycastle: ^1.0.0 9 | base58check: ^1.0.1 10 | convert: ^2.1.0 11 | 12 | dev_dependencies: 13 | pedantic: ^1.9.0 14 | test: ^1.5.1 15 | 16 | environment: 17 | sdk: ">=2.3.0 <3.0.0" 18 | -------------------------------------------------------------------------------- /test/bitcoin_bip32_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | 3 | import 'package:bitcoin_bip32/bitcoin_bip32.dart'; 4 | import 'package:bitcoin_bip32/src/crypto.dart'; 5 | 6 | // ignore_for_file: omit_local_variable_types 7 | 8 | void main() { 9 | const Map vector1 = { 10 | 'seed': '000102030405060708090a0b0c0d0e0f', 11 | 'chains': [ 12 | { 13 | 'chain': 'm', 14 | 'publicKey': 15 | 'xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8', 16 | 'privateKey': 17 | 'xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi', 18 | }, 19 | { 20 | 'chain': "m/0'", 21 | 'publicKey': 22 | 'xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw', 23 | 'privateKey': 24 | 'xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7', 25 | }, 26 | { 27 | 'chain': "m/0'/1", 28 | 'publicKey': 29 | 'xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ', 30 | 'privateKey': 31 | 'xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs', 32 | }, 33 | { 34 | 'chain': "m/0'/1/2'", 35 | 'publicKey': 36 | 'xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5', 37 | 'privateKey': 38 | 'xprv9z4pot5VBttmtdRTWfWQmoH1taj2axGVzFqSb8C9xaxKymcFzXBDptWmT7FwuEzG3ryjH4ktypQSAewRiNMjANTtpgP4mLTj34bhnZX7UiM', 39 | }, 40 | { 41 | 'chain': "m/0'/1/2'/2", 42 | 'publicKey': 43 | 'xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV', 44 | 'privateKey': 45 | 'xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334', 46 | }, 47 | { 48 | 'chain': "m/0'/1/2'/2/1000000000", 49 | 'publicKey': 50 | 'xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy', 51 | 'privateKey': 52 | 'xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76', 53 | }, 54 | ] 55 | }; 56 | 57 | const Map vector2 = { 58 | 'seed': 59 | 'fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542', 60 | 'chains': [ 61 | { 62 | 'chain': 'm', 63 | 'publicKey': 64 | 'xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB', 65 | 'privateKey': 66 | 'xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U', 67 | }, 68 | { 69 | 'chain': 'm/0', 70 | 'publicKey': 71 | 'xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH', 72 | 'privateKey': 73 | 'xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt', 74 | }, 75 | { 76 | 'chain': "m/0/2147483647'", 77 | 'publicKey': 78 | 'xpub6ASAVgeehLbnwdqV6UKMHVzgqAG8Gr6riv3Fxxpj8ksbH9ebxaEyBLZ85ySDhKiLDBrQSARLq1uNRts8RuJiHjaDMBU4Zn9h8LZNnBC5y4a', 79 | 'privateKey': 80 | 'xprv9wSp6B7kry3Vj9m1zSnLvN3xH8RdsPP1Mh7fAaR7aRLcQMKTR2vidYEeEg2mUCTAwCd6vnxVrcjfy2kRgVsFawNzmjuHc2YmYRmagcEPdU9', 81 | }, 82 | { 83 | 'chain': "m/0/2147483647'/1", 84 | 'publicKey': 85 | 'xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon', 86 | 'privateKey': 87 | 'xprv9zFnWC6h2cLgpmSA46vutJzBcfJ8yaJGg8cX1e5StJh45BBciYTRXSd25UEPVuesF9yog62tGAQtHjXajPPdbRCHuWS6T8XA2ECKADdw4Ef', 88 | }, 89 | { 90 | 'chain': "m/0/2147483647'/1/2147483646'", 91 | 'publicKey': 92 | 'xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL', 93 | 'privateKey': 94 | 'xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc', 95 | }, 96 | { 97 | 'chain': "m/0/2147483647'/1/2147483646'/2", 98 | 'publicKey': 99 | 'xpub6FnCn6nSzZAw5Tw7cgR9bi15UV96gLZhjDstkXXxvCLsUXBGXPdSnLFbdpq8p9HmGsApME5hQTZ3emM2rnY5agb9rXpVGyy3bdW6EEgAtqt', 100 | 'privateKey': 101 | 'xprvA2nrNbFZABcdryreWet9Ea4LvTJcGsqrMzxHx98MMrotbir7yrKCEXw7nadnHM8Dq38EGfSh6dqA9QWTyefMLEcBYJUuekgW4BYPJcr9E7j', 102 | }, 103 | ] 104 | }; 105 | 106 | const Map vector3 = { 107 | 'seed': 108 | '4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be', 109 | 'chains': [ 110 | { 111 | 'chain': 'm', 112 | 'publicKey': 113 | 'xpub661MyMwAqRbcEZVB4dScxMAdx6d4nFc9nvyvH3v4gJL378CSRZiYmhRoP7mBy6gSPSCYk6SzXPTf3ND1cZAceL7SfJ1Z3GC8vBgp2epUt13', 114 | 'privateKey': 115 | 'xprv9s21ZrQH143K25QhxbucbDDuQ4naNntJRi4KUfWT7xo4EKsHt2QJDu7KXp1A3u7Bi1j8ph3EGsZ9Xvz9dGuVrtHHs7pXeTzjuxBrCmmhgC6', 116 | }, 117 | { 118 | 'chain': "m/0'", 119 | 'publicKey': 120 | 'xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y', 121 | 'privateKey': 122 | 'xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L', 123 | }, 124 | ] 125 | }; 126 | 127 | [vector1, vector2, vector3].forEach((vector) { 128 | test('static vector', () { 129 | var chain = Chain.seed(vector['seed']); 130 | vector['chains'].forEach((child) { 131 | ExtendedPrivateKey privateKey = chain.forPath(child['chain']); 132 | expect(privateKey.toString(), child['privateKey']); 133 | 134 | var publicKey = privateKey.publicKey(); 135 | expect(publicKey.toString(), child['publicKey']); 136 | }); 137 | }); 138 | }); 139 | 140 | group('chain', () { 141 | test('throw exception when generating private key based on public key', () { 142 | var chain = Chain.import(vector1['chains'][0]['publicKey']); 143 | 144 | expect(() => chain.forPath('m'), throwsA(TypeMatcher())); 145 | }); 146 | 147 | test("throws exception when path doesn't start with 'm' or 'M'", () { 148 | var chain = Chain.import(vector1['chains'][0]['publicKey']); 149 | 150 | expect( 151 | () => chain.forPath('/foobar'), throwsA(TypeMatcher())); 152 | }); 153 | 154 | group('path parser', () { 155 | Chain chain; 156 | 157 | setUp(() { 158 | chain = Chain.seed('00'); 159 | }); 160 | 161 | test('ignores trailing slashes', () { 162 | var key1 = chain.forPath('m/100'); 163 | var key2 = chain.forPath('m/100/'); 164 | 165 | expect(key1.toString(), key2.toString()); 166 | }); 167 | 168 | group('m', () { 169 | var key; 170 | setUp(() { 171 | key = chain.forPath('m'); 172 | }); 173 | 174 | test('has depth 0', () { 175 | expect(key.depth, 0); 176 | }); 177 | 178 | test('has child number 0', () { 179 | expect(key.childNumber, 0); 180 | }); 181 | 182 | test('is a private key', () { 183 | expect(key, TypeMatcher()); 184 | }); 185 | }); 186 | 187 | group('M', () { 188 | var key; 189 | setUp(() { 190 | key = chain.forPath('M'); 191 | }); 192 | 193 | test('has depth 0', () { 194 | expect(key.depth, 0); 195 | }); 196 | 197 | test('has child number 0', () { 198 | expect(key.childNumber, 0); 199 | }); 200 | 201 | test('is a public key', () { 202 | expect(key, TypeMatcher()); 203 | }); 204 | }); 205 | 206 | group('m/100', () { 207 | var key; 208 | setUp(() { 209 | key = chain.forPath('m/100'); 210 | }); 211 | 212 | test('has depth 1', () { 213 | expect(key.depth, 1); 214 | }); 215 | 216 | test('has child number 100', () { 217 | expect(key.childNumber, 100); 218 | }); 219 | }); 220 | 221 | group("m/100'", () { 222 | var key; 223 | setUp(() { 224 | key = chain.forPath("m/100'"); 225 | }); 226 | 227 | test('has depth 1', () { 228 | expect(key.depth, 1); 229 | }); 230 | 231 | test('has child number 2147483648 + 100', () { 232 | expect(key.childNumber, firstHardenedChild + 100); 233 | }); 234 | }); 235 | 236 | group("m/100'/0", () { 237 | var key; 238 | setUp(() { 239 | key = chain.forPath("m/100'/0"); 240 | }); 241 | 242 | test('has depth 2', () { 243 | expect(key.depth, 2); 244 | }); 245 | 246 | test('has child number 0', () { 247 | expect(key.childNumber, 0); 248 | }); 249 | }); 250 | }); 251 | }); 252 | 253 | test('refuse to generate a hardened child for a extended public key', () { 254 | ExtendedPublicKey parent = 255 | ExtendedKey.deserialize(vector2['chains'][0]['publicKey']); 256 | 257 | expect(() => deriveExtendedPublicChildKey(parent, firstHardenedChild), 258 | throwsA(TypeMatcher())); 259 | }); 260 | 261 | group('(de)serialization', () { 262 | test('private master key', () { 263 | var serializedKey = 264 | 'xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi'; 265 | 266 | expect(ExtendedKey.deserialize(serializedKey).toString(), serializedKey); 267 | }); 268 | 269 | test('public master key', () { 270 | var serializedKey = 271 | 'xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8'; 272 | 273 | expect(ExtendedKey.deserialize(serializedKey).toString(), serializedKey); 274 | }); 275 | 276 | test('private child key', () { 277 | var serializedKey = 278 | 'xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7'; 279 | 280 | expect(ExtendedKey.deserialize(serializedKey).toString(), serializedKey); 281 | }); 282 | 283 | test('public child key', () { 284 | var serializedKey = 285 | 'xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw'; 286 | 287 | expect(ExtendedKey.deserialize(serializedKey).toString(), serializedKey); 288 | }); 289 | 290 | test('broken checksum for private key', () { 291 | // (Capitalized a random character from the private master key) 292 | var serializedKey = 293 | 'xprv9s21ZrQH143K3QTDL4LXw2F7HEK3WJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi'; 294 | 295 | expect(() => ExtendedKey.deserialize(serializedKey), 296 | throwsA(TypeMatcher())); 297 | }); 298 | 299 | test('broken checksum for public key', () { 300 | // (Capitalized a random character from the public master key) 301 | var serializedKey = 302 | 'xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7uSUDFdp6W1EGMcet8'; 303 | 304 | expect(() => ExtendedKey.deserialize(serializedKey), 305 | throwsA(TypeMatcher())); 306 | }); 307 | 308 | test('too short a key', () { 309 | var serializedKey = 'xpubx'; 310 | 311 | expect(() => ExtendedKey.deserialize(serializedKey), 312 | throwsA(TypeMatcher())); 313 | }); 314 | 315 | test('too long a key', () { 316 | var serializedKey = 317 | 'xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnwAAAAAAAAAAAAAAAAAAAAAAAAAAAA'; 318 | 319 | expect(() => ExtendedKey.deserialize(serializedKey), 320 | throwsA(TypeMatcher())); 321 | }); 322 | }); 323 | } 324 | --------------------------------------------------------------------------------