diff --git a/lib/src/crypto/hd_wallet/extended_private_key.dart b/lib/src/crypto/hd_wallet/extended_private_key.dart index c875eb4..2c6a215 100644 --- a/lib/src/crypto/hd_wallet/extended_private_key.dart +++ b/lib/src/crypto/hd_wallet/extended_private_key.dart @@ -1,17 +1,15 @@ import 'dart:typed_data'; -import 'package:witnet/src/crypto/hd_wallet/extended_key.dart'; -import 'package:witnet/src/crypto/secp256k1/secp256k1.dart'; -import 'package:witnet/src/utils/bech32/exceptions.dart'; import 'package:witnet/src/crypto/address.dart'; -import 'package:witnet/src/crypto/encrypt/aes/exceptions.dart'; -import 'package:witnet/src/crypto/encrypt/aes/codec.dart'; -import 'package:witnet/src/utils/bech32/bech32.dart'; import 'package:witnet/src/crypto/bip39/bip39.dart' as bip39; - import 'package:witnet/src/crypto/crypto.dart'; +import 'package:witnet/src/crypto/encrypt/aes/codec.dart'; +import 'package:witnet/src/crypto/encrypt/aes/exceptions.dart'; +import 'package:witnet/src/crypto/hd_wallet/extended_key.dart'; import 'package:witnet/src/crypto/secp256k1/private_key.dart'; - +import 'package:witnet/src/crypto/secp256k1/secp256k1.dart'; +import 'package:witnet/src/utils/bech32/bech32.dart'; +import 'package:witnet/src/utils/bech32/exceptions.dart'; import 'package:witnet/utils.dart'; import 'extended_public_key.dart'; @@ -145,6 +143,20 @@ class Xprv extends ExtendedKey { return bech32.encoder.convert(Bech32(hrp: 'xprv', data: _data)); } + String toIndexAwareSlip32( + {required int externalIndex, required int internalIndex}) { + Uint8List depthBytes = bigIntToBytes(BigInt.from(depth)); + Uint8List padding = leftJustify(Uint8List.fromList([0]), 1, 0); + Uint8List ext = + hexToBytes(bigIntToHex(BigInt.from(externalIndex)).padLeft(8, '0')); + Uint8List int = + hexToBytes(bigIntToHex(BigInt.from(internalIndex)).padLeft(8, '0')); + Uint8List data = concatBytes( + [depthBytes, padding, code, padding, privateKey.bytes.bytes, ext, int]); + var _data = convertBits(data: data, from: 8, to: 5, pad: true); + return bech32.encoder.convert(Bech32(hrp: 'xprv', data: _data)); + } + factory Xprv.fromEncryptedXprv(String xprv, String password) { try { Bech32 bech = bech32.decode(xprv); @@ -176,6 +188,107 @@ class Xprv extends ExtendedKey { } } + String toIndexAwareEncryptedXprv( + String password, int externalIndex, int internalIndex) { + String xprvStr = toIndexAwareSlip32( + externalIndex: externalIndex, internalIndex: internalIndex); + List tmp = xprvStr.codeUnits; + Uint8List dat = Uint8List(144); + int padLength = dat.length - tmp.length; + Uint8List padding = Uint8List(padLength); + for (int i = 0; i < padLength; i++) { + padding[i] = 11; + } + dat.setRange(0, tmp.length, tmp); + dat.setRange(tmp.length, dat.length, padding); + + Uint8List _iv = generateIV(); + Uint8List _salt = generateSalt(); + CodecAES codec = getCodecAES(password, salt: _salt, iv: _iv); + final encoded = codec.encode(dat); + Uint8List encData = concatBytes([_iv, _salt, hexToBytes(encoded)]); + + Uint8List data1 = Uint8List.fromList( + convertBits(data: encData, from: 8, to: 5, pad: true)); + + Bech32 b1 = Bech32(hrp: 'xprv', data: data1); + + String bech1 = bech32.encode(b1, 319); + + // 32bit to 256bit + return bech1; + } + + static List fromIndexAwareXprv(String xprv) { + /// An index aware XPRV includes the last internal and external + /// wallet address index. they are stored as 4 bytes each. + Bech32 bech = bech32.decoder.convert(xprv, 130); + try { + List checksum = createChecksum(bech.hrp, bech.data); + bool invalidXprvChecksum = + !verifyChecksum(bech.hrp, [...bech.data, ...checksum]); + if (invalidXprvChecksum) { + throw InvalidChecksum(); + } + } catch (e) { + rethrow; + } + + var bn256 = convertBits(data: bech.data, from: 5, to: 8, pad: true); + Uint8List data = Uint8List.fromList(bn256); + String hrp = bech.hrp; + assert(hrp == 'xprv', 'Not a valid XPRV for importing.'); + Uint8List depth = data.sublist(0, 1); + Uint8List chainCode = data.sublist(1, 33); + Uint8List keyData = data.sublist(34, 66); + int externalIndex = bytesToBigInt(data.sublist(66, 70)).toInt(); + int internalIndex = bytesToBigInt(data.sublist(70, 74)).toInt(); + bool masterKey = false; + if (4 * depth[0] == 0) { + masterKey = true; + } + Xprv _xprv = Xprv( + key: keyData, + code: chainCode, + depth: depth[0], + masterKey: masterKey, + path: null, + index: null, + parent: null); + return [_xprv, externalIndex, internalIndex]; + } + + static List fromIndexAwareEncryptedXprv( + String xprv, String password) { + try { + Bech32 bech = bech32.decode(xprv, 319); + + Uint8List data = Uint8List.fromList( + convertBits(data: bech.data, from: 5, to: 8, pad: false)); + + Uint8List iv = data.sublist(0, 16); + Uint8List salt = data.sublist(16, 48); + Uint8List _data = data.sublist(48); + + CodecAES codec = getCodecAES(password, salt: salt, iv: iv); + Uint8List decoded = codec.decode(bytesToHex(_data)) as Uint8List; + decoded = decoded.sublist(0, 130); + String plainText; + plainText = utf8.decode(decoded).trim(); + List indexAwareXprv = Xprv.fromIndexAwareXprv(plainText); + Xprv _xprv = indexAwareXprv[0]; + int lastExternalIndex = indexAwareXprv[1]; + int lastInternalIndex = indexAwareXprv[2]; + + return [_xprv, lastExternalIndex, lastInternalIndex]; + } catch (e) { + if (e.runtimeType == InvalidChecksum) { + rethrow; + } + throw AesCryptDataException('${e.toString()}'); + } + } + String toEncryptedXprv({required String password}) { String xprvStr = toSlip32(); List tmp = xprvStr.codeUnits; diff --git a/test/extended_key_test.dart b/test/extended_key_test.dart index 293296e..c875e61 100644 --- a/test/extended_key_test.dart +++ b/test/extended_key_test.dart @@ -1,9 +1,13 @@ +import 'package:test/test.dart'; import 'package:witnet/constants.dart'; import 'package:witnet/witnet.dart'; -import 'package:test/test.dart'; String mnemonic = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'; + +String masterNodeXprv = + "xprv1qpujxsyd4hfu0dtwa524vac84e09mjsgnh5h9crl8wrqg58z5wmsuqqcxlqmar3fjhkprndzkpnp2xlze76g4hu7g7c4r4r2m2e6y8xlvu566tn6"; + List> expectedAddresses = [ ["m/3h/4919h/0h/0/0", "wit174la8pevl74hczcpfepgmt036zkmjen4hu8zzs"], ["m/3h/4919h/0h/0/1", "wit1cetlhcpqc3jxqxap6egql5py4jrgwnfzfsm6l7"], @@ -25,6 +29,14 @@ main() async { test('XPUB', () { expect(xpubTest(), true); }); + + test('Index Aware XPRV', () { + expect(indexAwareXprvTest(), true); + }); + + test('Encrypted Index Aware XPRV', () { + expect(encryptedIndexAwareXprvTest(), true); + }); } bool xprvTest() { @@ -51,3 +63,63 @@ bool xpubTest() { } return true; } + +bool indexAwareXprvTest() { + Xprv xprv = Xprv.fromMnemonic(mnemonic: mnemonic); + + /// Test Zero indexes + String xprvString = + "xprv1qpujxsyd4hfu0dtwa524vac84e09mjsgnh5h9crl8wrqg58z5wmsuqqcxlqmar3fjhkprndzkpnp2xlze76g4hu7g7c4r4r2m2e6y8xlvuqqqqqqqqqqqqqs0qn8x"; + assert(xprvString == + xprv.toIndexAwareSlip32(externalIndex: 0, internalIndex: 0)); + + List indexedXprvData = Xprv.fromIndexAwareXprv(xprvString); + + Xprv indexedXprv = indexedXprvData[0]; + int externalIndex = indexedXprvData[1]; + int internalIndex = indexedXprvData[2]; + + assert(masterNodeXprv == indexedXprv.toSlip32()); + assert(externalIndex == 0); + assert(internalIndex == 0); + + /// Test other indexes + xprvString = + "xprv1qpujxsyd4hfu0dtwa524vac84e09mjsgnh5h9crl8wrqg58z5wmsuqqcxlqmar3fjhkprndzkpnp2xlze76g4hu7g7c4r4r2m2e6y8xlvuqqqqfvqqqqraq829yg8"; + int expectedExternalIndex = 300; + int expectedInternalIndex = 500; + assert(xprvString == + xprv.toIndexAwareSlip32( + externalIndex: expectedExternalIndex, + internalIndex: expectedInternalIndex)); + + indexedXprvData = Xprv.fromIndexAwareXprv(xprvString); + + indexedXprv = indexedXprvData[0]; + externalIndex = indexedXprvData[1]; + internalIndex = indexedXprvData[2]; + + assert(masterNodeXprv == indexedXprv.toSlip32()); + assert(externalIndex == expectedExternalIndex); + assert(internalIndex == expectedInternalIndex); + + return true; + + /// Test Encrypted Index Aware XPRV +} + +bool encryptedIndexAwareXprvTest() { + String xprvString = + "xprv19aer5vfuyaqt4ljlzarpymqasskdv7kwh3kmfp5m78wxjfdpgx7gvht95px38ylh7tl2xqgwezf3fujcupfkj64hzdqvwd0y3txhk7yk6xxd77dal0klxlfh854n6laxh6yvl24cqa7k9d8ljkn6x0ly223hu8l4ywpvmtdpg6mhs6mrhg05auqq2hl58c2v5y9grdzatmtarxeprka8udtlwzpnpfg8enrlt20ne8n4h5j7a4kf8wa4zvcad4wwdc7v53w6wqc286jnevm8kydtc9jfr4gt3mxsm3mqvv8zk5nzs92q38fmtv"; + List indexedXprvData = + Xprv.fromIndexAwareEncryptedXprv(xprvString, "password"); + Xprv indexedXprv = indexedXprvData[0]; + int externalIndex = indexedXprvData[1]; + int internalIndex = indexedXprvData[2]; + + assert(masterNodeXprv == indexedXprv.toSlip32()); + assert(externalIndex == 0); + assert(internalIndex == 0); + + return true; +}