├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── example └── eosdart_ecc_example.dart ├── lib ├── eosdart_ecc.dart └── src │ ├── exception.dart │ ├── key.dart │ ├── key_base.dart │ └── signature.dart ├── pubspec.yaml └── test ├── eosdart_ecc_test.dart ├── sign_string_test.dart └── signature_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub 2 | .dart_tool/ 3 | .packages 4 | # Remove the following pattern if you wish to check in your lock file 5 | pubspec.lock 6 | 7 | # Conventional directory for build outputs 8 | build/ 9 | 10 | # Directory created by dartdoc 11 | doc/api/ 12 | 13 | # IntelliJ / Android Studio 14 | .idea 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: dart 2 | dart_task: 3 | - dartfmt: sdk 4 | - test 5 | - dartanalyzer 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.4.4 2 | 3 | - Null safety 4 | 5 | ## 0.4.3 6 | 7 | - Clean up 8 | 9 | ## 0.4.2 10 | 11 | - Support recover public key from signature 12 | 13 | ## 0.4.1 14 | 15 | - Adopt Flutter Web 16 | 17 | ## 0.4.0 18 | 19 | - fix signature bug 20 | 21 | ## 0.3.2 22 | 23 | - fix signature bug 24 | 25 | ## 0.3.1 26 | 27 | - sign method will sign Uint8List 28 | - add signString method which signs String 29 | 30 | ## 0.3.0 31 | 32 | - Support canonical signature 33 | 34 | ## 0.2.2 35 | 36 | - Healthy change 37 | 38 | ## 0.2.1 39 | 40 | - Add CHANGELOG.md 41 | 42 | ## 0.2.0 43 | 44 | - EOSPublicKey, EOSPrivateKey, EOSSignature 45 | - Sign the data 46 | - Verify the signature 47 | 48 | ## 0.1.0 49 | 50 | - Initial version, created by Stagehand 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Primes Network 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elliptic curve cryptography (ECC) in Dart 2 | 3 | Elliptic curve cryptography lib for EOS based blockchain in Dart lang. 4 | 5 | [![Build Status](https://travis-ci.com/primes-network/eosdart_ecc.svg?branch=master)](https://travis-ci.com/primes-network/eosdart_ecc) 6 | 7 | 8 | ## Usage 9 | 10 | A simple usage example: 11 | 12 | ```dart 13 | import 'package:eosdart_ecc/eosdart_ecc.dart'; 14 | 15 | main() { 16 | // Construct the EOS private key from string 17 | EOSPrivateKey privateKey = EOSPrivateKey.fromString( 18 | '5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3'); 19 | 20 | // Get the related EOS public key 21 | EOSPublicKey publicKey = privateKey.toEOSPublicKey(); 22 | // Print the EOS public key 23 | print(publicKey.toString()); 24 | 25 | // Going to sign the data 26 | String data = 'data'; 27 | 28 | // Sign 29 | EOSSignature signature = privateKey.signString(data); 30 | // Print the EOS signature 31 | print(signature.toString()); 32 | 33 | // Verify the data using the signature 34 | signature.verify(data, publicKey); 35 | } 36 | ``` 37 | 38 | ## Features and bugs 39 | 40 | Please file feature requests and bugs at the [issue tracker][tracker]. 41 | 42 | ## References 43 | 44 | eosjs-ecc: https://github.com/EOSIO/eosjs-ecc 45 | 46 | [tracker]: https://github.com/primes-network/eosdart_ecc/issues 47 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | analyzer: 2 | # exclude: 3 | # - path/to/excluded/files/** 4 | 5 | # Lint rules and documentation, see http://dart-lang.github.io/linter/lints 6 | linter: 7 | rules: 8 | - cancel_subscriptions 9 | - hash_and_equals 10 | - iterable_contains_unrelated_type 11 | - list_remove_unrelated_type 12 | - test_types_in_equals 13 | - unrelated_type_equality_checks 14 | - valid_regexps 15 | -------------------------------------------------------------------------------- /example/eosdart_ecc_example.dart: -------------------------------------------------------------------------------- 1 | import 'package:eosdart_ecc/eosdart_ecc.dart'; 2 | 3 | main() { 4 | // Construct the EOS private key from string 5 | EOSPrivateKey privateKey = EOSPrivateKey.fromString( 6 | '5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3'); 7 | 8 | // Get the related EOS public key 9 | EOSPublicKey publicKey = privateKey.toEOSPublicKey(); 10 | // Print the EOS public key 11 | print(publicKey.toString()); 12 | 13 | // Going to sign the data 14 | String data = 'data'; 15 | 16 | // Sign 17 | EOSSignature signature = privateKey.signString(data); 18 | // Print the EOS signature 19 | print(signature.toString()); 20 | 21 | // Recover the EOSPublicKey used to sign the data 22 | var recoveredEOSPublicKey = signature.recover(data); 23 | print(recoveredEOSPublicKey.toString()); 24 | 25 | // Verify the data using the signature 26 | signature.verify(data, publicKey); 27 | } 28 | -------------------------------------------------------------------------------- /lib/eosdart_ecc.dart: -------------------------------------------------------------------------------- 1 | /// Elliptic curve cryptography lib for EOS based blockchain in Dart lang. 2 | /// 3 | library eosdart_ecc; 4 | 5 | export 'src/key.dart'; 6 | export 'src/signature.dart'; 7 | export 'src/exception.dart'; 8 | -------------------------------------------------------------------------------- /lib/src/exception.dart: -------------------------------------------------------------------------------- 1 | class InvalidKey implements Exception { 2 | String cause; 3 | 4 | InvalidKey(this.cause); 5 | 6 | String toString() => cause; 7 | } 8 | -------------------------------------------------------------------------------- /lib/src/key.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:math'; 3 | import 'dart:typed_data'; 4 | 5 | import 'package:crypto/crypto.dart'; 6 | import 'package:pointycastle/ecc/api.dart' show ECSignature, ECPoint; 7 | import 'package:pointycastle/src/utils.dart'; 8 | 9 | import './exception.dart'; 10 | import './key_base.dart'; 11 | import './signature.dart'; 12 | 13 | /// EOS Public Key 14 | class EOSPublicKey extends EOSKey { 15 | ECPoint? q; 16 | 17 | /// Construct EOS public key from buffer 18 | EOSPublicKey.fromPoint(this.q); 19 | 20 | /// Construct EOS public key from string 21 | factory EOSPublicKey.fromString(String keyStr) { 22 | RegExp publicRegex = RegExp(r"^PUB_([A-Za-z0-9]+)_([A-Za-z0-9]+)", 23 | caseSensitive: true, multiLine: false); 24 | Iterable match = publicRegex.allMatches(keyStr); 25 | 26 | if (match.isEmpty) { 27 | RegExp eosRegex = RegExp(r"^EOS", caseSensitive: true, multiLine: false); 28 | if (!eosRegex.hasMatch(keyStr)) { 29 | throw InvalidKey("No leading EOS"); 30 | } 31 | String publicKeyStr = keyStr.substring(3); 32 | Uint8List buffer = EOSKey.decodeKey(publicKeyStr); 33 | return EOSPublicKey.fromBuffer(buffer); 34 | } else if (match.length == 1) { 35 | Match m = match.first; 36 | String? keyType = m.group(1); 37 | Uint8List buffer = EOSKey.decodeKey(m.group(2)!, keyType); 38 | return EOSPublicKey.fromBuffer(buffer); 39 | } else { 40 | throw InvalidKey('Invalid public key format'); 41 | } 42 | } 43 | 44 | factory EOSPublicKey.fromBuffer(Uint8List buffer) { 45 | ECPoint? point = EOSKey.secp256k1.curve.decodePoint(buffer); 46 | return EOSPublicKey.fromPoint(point); 47 | } 48 | 49 | Uint8List toBuffer() { 50 | // always compressed 51 | return q!.getEncoded(true); 52 | } 53 | 54 | String toString() { 55 | return 'EOS' + EOSKey.encodeKey(this.toBuffer(), keyType); 56 | } 57 | } 58 | 59 | /// EOS Private Key 60 | class EOSPrivateKey extends EOSKey { 61 | Uint8List? d; 62 | String? format; 63 | 64 | late BigInt _r; 65 | late BigInt _s; 66 | 67 | /// Constructor EOS private key from the key buffer itself 68 | EOSPrivateKey.fromBuffer(this.d); 69 | 70 | /// Construct the private key from string 71 | /// It can come from WIF format for PVT format 72 | EOSPrivateKey.fromString(String keyStr) { 73 | RegExp privateRegex = RegExp(r"^PVT_([A-Za-z0-9]+)_([A-Za-z0-9]+)", 74 | caseSensitive: true, multiLine: false); 75 | Iterable match = privateRegex.allMatches(keyStr); 76 | 77 | if (match.isEmpty) { 78 | format = 'WIF'; 79 | keyType = 'K1'; 80 | // WIF 81 | Uint8List keyWLeadingVersion = EOSKey.decodeKey(keyStr, EOSKey.SHA256X2); 82 | int version = keyWLeadingVersion.first; 83 | if (EOSKey.VERSION != version) { 84 | throw InvalidKey("version mismatch"); 85 | } 86 | 87 | d = keyWLeadingVersion.sublist(1, keyWLeadingVersion.length); 88 | if (d!.lengthInBytes == 33 && d!.elementAt(32) == 1) { 89 | // remove compression flag 90 | d = d!.sublist(0, 32); 91 | } 92 | 93 | if (d!.lengthInBytes != 32) { 94 | throw InvalidKey('Expecting 32 bytes, got ${d!.length}'); 95 | } 96 | } else if (match.length == 1) { 97 | format = 'PVT'; 98 | Match m = match.first; 99 | keyType = m.group(1); 100 | d = EOSKey.decodeKey(m.group(2)!, keyType); 101 | } else { 102 | throw InvalidKey('Invalid Private Key format'); 103 | } 104 | } 105 | 106 | /// Generate EOS private key from seed. Please note: This is not random! 107 | /// For the given seed, the generated key would always be the same 108 | factory EOSPrivateKey.fromSeed(String seed) { 109 | Digest s = sha256.convert(utf8.encode(seed)); 110 | return EOSPrivateKey.fromBuffer(Uint8List.fromList(s.bytes)); 111 | } 112 | 113 | /// Generate the random EOS private key 114 | factory EOSPrivateKey.fromRandom() { 115 | // final int randomLimit = 1 << 32; 116 | final int randomLimit = 4294967296; 117 | Random randomGenerator; 118 | try { 119 | randomGenerator = Random.secure(); 120 | } catch (e) { 121 | randomGenerator = Random(); 122 | } 123 | 124 | int randomInt1 = randomGenerator.nextInt(randomLimit); 125 | Uint8List entropy1 = encodeBigInt(BigInt.from(randomInt1)); 126 | 127 | int randomInt2 = randomGenerator.nextInt(randomLimit); 128 | Uint8List entropy2 = encodeBigInt(BigInt.from(randomInt2)); 129 | 130 | int randomInt3 = randomGenerator.nextInt(randomLimit); 131 | Uint8List entropy3 = encodeBigInt(BigInt.from(randomInt3)); 132 | 133 | List entropy = entropy1.toList(); 134 | entropy.addAll(entropy2); 135 | entropy.addAll(entropy3); 136 | Uint8List randomKey = Uint8List.fromList(entropy); 137 | Digest d = sha256.convert(randomKey); 138 | return EOSPrivateKey.fromBuffer(Uint8List.fromList(d.bytes)); 139 | } 140 | 141 | /// Check if the private key is WIF format 142 | bool isWIF() => this.format == 'WIF'; 143 | 144 | /// Get the public key string from this private key 145 | EOSPublicKey toEOSPublicKey() { 146 | BigInt privateKeyNum = decodeBigIntWithSign(1, this.d!); 147 | ECPoint? ecPoint = EOSKey.secp256k1.G * privateKeyNum; 148 | 149 | return EOSPublicKey.fromPoint(ecPoint); 150 | } 151 | 152 | /// Sign the bytes data using the private key 153 | EOSSignature sign(Uint8List data) { 154 | Digest d = sha256.convert(data); 155 | return signHash(Uint8List.fromList(d.bytes)); 156 | } 157 | 158 | /// Sign the string data using the private key 159 | EOSSignature signString(String data) { 160 | return sign(Uint8List.fromList(utf8.encode(data))); 161 | } 162 | 163 | /// Sign the SHA256 hashed data using the private key 164 | EOSSignature signHash(Uint8List sha256Data) { 165 | int nonce = 0; 166 | BigInt n = EOSKey.secp256k1.n; 167 | BigInt e = decodeBigIntWithSign(1, sha256Data); 168 | 169 | while (true) { 170 | _deterministicGenerateK(sha256Data, this.d!, e, nonce++); 171 | var N_OVER_TWO = n >> 1; 172 | if (_s.compareTo(N_OVER_TWO) > 0) { 173 | _s = n - _s; 174 | } 175 | ECSignature sig = ECSignature(_r, _s); 176 | 177 | Uint8List der = EOSSignature.ecSigToDER(sig); 178 | 179 | int lenR = der.elementAt(3); 180 | int lenS = der.elementAt(5 + lenR); 181 | if (lenR == 32 && lenS == 32) { 182 | int i = EOSSignature.calcPubKeyRecoveryParam( 183 | decodeBigIntWithSign(1, sha256Data), sig, this.toEOSPublicKey()); 184 | i += 4; // compressed 185 | i += 27; // compact // 24 or 27 :( forcing odd-y 2nd key candidate) 186 | return EOSSignature(i, sig.r, sig.s); 187 | } 188 | } 189 | } 190 | 191 | String toString() { 192 | List version = []; 193 | version.add(EOSKey.VERSION); 194 | Uint8List keyWLeadingVersion = 195 | EOSKey.concat(Uint8List.fromList(version), this.d!); 196 | 197 | return EOSKey.encodeKey(keyWLeadingVersion, EOSKey.SHA256X2); 198 | } 199 | 200 | BigInt _deterministicGenerateK( 201 | Uint8List hash, Uint8List x, BigInt e, int nonce) { 202 | List newHash = hash; 203 | if (nonce > 0) { 204 | List addition = Uint8List(nonce); 205 | List data = List.from(hash)..addAll(addition); 206 | newHash = sha256.convert(data).bytes; 207 | } 208 | 209 | // Step B 210 | Uint8List v = Uint8List(32); 211 | for (int i = 0; i < v.lengthInBytes; i++) { 212 | v[i] = 1; 213 | } 214 | 215 | // Step C 216 | Uint8List k = Uint8List(32); 217 | 218 | // Step D 219 | List d1 = List.from(v) 220 | ..add(0) 221 | ..addAll(x) 222 | ..addAll(newHash); 223 | 224 | Hmac hMacSha256 = Hmac(sha256, k); // HMAC-SHA256 225 | k = Uint8List.fromList(hMacSha256.convert(d1).bytes); 226 | 227 | // Step E 228 | hMacSha256 = Hmac(sha256, k); // HMAC-SHA256 229 | v = Uint8List.fromList(hMacSha256.convert(v).bytes); 230 | 231 | // Step F 232 | List d2 = List.from(v) 233 | ..add(1) 234 | ..addAll(x) 235 | ..addAll(newHash); 236 | 237 | k = Uint8List.fromList(hMacSha256.convert(d2).bytes); 238 | 239 | // Step G 240 | hMacSha256 = Hmac(sha256, k); // HMAC-SHA256 241 | v = Uint8List.fromList(hMacSha256.convert(v).bytes); 242 | // Step H1/H2a, again, ignored as tlen === qlen (256 bit) 243 | // Step H2b again 244 | v = Uint8List.fromList(hMacSha256.convert(v).bytes); 245 | 246 | BigInt T = decodeBigIntWithSign(1, v); 247 | // Step H3, repeat until T is within the interval [1, n - 1] 248 | while (T.sign <= 0 || 249 | T.compareTo(EOSKey.secp256k1.n) >= 0 || 250 | !_checkSig(e, Uint8List.fromList(newHash), T)) { 251 | List d3 = List.from(v)..add(0); 252 | k = Uint8List.fromList(hMacSha256.convert(d3).bytes); 253 | hMacSha256 = Hmac(sha256, k); // HMAC-SHA256 254 | v = Uint8List.fromList(hMacSha256.convert(v).bytes); 255 | // Step H1/H2a, again, ignored as tlen === qlen (256 bit) 256 | // Step H2b again 257 | v = Uint8List.fromList(hMacSha256.convert(v).bytes); 258 | 259 | T = decodeBigIntWithSign(1, v); 260 | } 261 | return T; 262 | } 263 | 264 | bool _checkSig(BigInt e, Uint8List hash, BigInt k) { 265 | BigInt n = EOSKey.secp256k1.n; 266 | ECPoint Q = (EOSKey.secp256k1.G * k)!; 267 | 268 | if (Q.isInfinity) { 269 | return false; 270 | } 271 | 272 | _r = Q.x!.toBigInteger()! % n; 273 | if (_r.sign == 0) { 274 | return false; 275 | } 276 | 277 | _s = k.modInverse(EOSKey.secp256k1.n) * 278 | (e + decodeBigIntWithSign(1, d!) * _r) % 279 | n; 280 | if (_s.sign == 0) { 281 | return false; 282 | } 283 | 284 | return true; 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /lib/src/key_base.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:bs58check/bs58check.dart'; 5 | import 'package:crypto/crypto.dart'; 6 | import 'package:pointycastle/digests/ripemd160.dart'; 7 | import "package:pointycastle/ecc/curves/secp256k1.dart"; 8 | import 'package:pointycastle/src/utils.dart'; 9 | 10 | import './exception.dart'; 11 | 12 | /// abstract EOS Key 13 | abstract class EOSKey { 14 | static final String SHA256X2 = 'sha256x2'; 15 | static final int VERSION = 0x80; 16 | static final ECCurve_secp256k1 secp256k1 = ECCurve_secp256k1(); 17 | 18 | String? keyType; 19 | 20 | /// Decode key from string format 21 | static Uint8List decodeKey(String keyStr, [String? keyType]) { 22 | Uint8List buffer = base58.decode(keyStr); 23 | 24 | Uint8List checksum = buffer.sublist(buffer.length - 4, buffer.length); 25 | Uint8List key = buffer.sublist(0, buffer.length - 4); 26 | 27 | Uint8List newChecksum; 28 | if (keyType == SHA256X2) { 29 | newChecksum = sha256x2(key).sublist(0, 4); 30 | } else { 31 | Uint8List check = key; 32 | if (keyType != null) { 33 | check = concat(key, Uint8List.fromList(utf8.encode(keyType))); 34 | } 35 | newChecksum = RIPEMD160Digest().process(check).sublist(0, 4); 36 | } 37 | if (decodeBigIntWithSign(1, checksum) != 38 | decodeBigIntWithSign(1, newChecksum)) { 39 | throw InvalidKey("checksum error"); 40 | } 41 | return key; 42 | } 43 | 44 | /// Encode key to string format using base58 encoding 45 | static String encodeKey(Uint8List key, [String? keyType]) { 46 | if (keyType == SHA256X2) { 47 | Uint8List checksum = sha256x2(key).sublist(0, 4); 48 | return base58.encode(concat(key, checksum)); 49 | } 50 | 51 | Uint8List keyBuffer = key; 52 | if (keyType != null) { 53 | keyBuffer = concat(key, Uint8List.fromList(utf8.encode(keyType))); 54 | } 55 | Uint8List checksum = RIPEMD160Digest().process(keyBuffer).sublist(0, 4); 56 | return base58.encode(concat(key, checksum)); 57 | } 58 | 59 | /// Do SHA256 hash twice on the given data 60 | static Uint8List sha256x2(Uint8List data) { 61 | Digest d1 = sha256.convert(data); 62 | Digest d2 = sha256.convert(d1.bytes); 63 | return Uint8List.fromList(d2.bytes); 64 | } 65 | 66 | static Uint8List concat(Uint8List p1, Uint8List p2) { 67 | List keyList = p1.toList(); 68 | keyList.addAll(p2); 69 | return Uint8List.fromList(keyList); 70 | } 71 | 72 | static List toSigned(Uint8List bytes) { 73 | List result = []; 74 | for (int i = 0; i < bytes.length; i++) { 75 | int v = bytes[i].toSigned(8); 76 | //TODO I don't know why, just guess... 77 | if (i == 0 && v < 0) { 78 | result.add(0); 79 | } 80 | result.add(v); 81 | } 82 | return result; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/src/signature.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:math'; 3 | import 'dart:typed_data'; 4 | 5 | import 'package:crypto/crypto.dart'; 6 | import "package:pointycastle/api.dart" show PublicKeyParameter; 7 | import "package:pointycastle/digests/sha256.dart"; 8 | import 'package:pointycastle/ecc/api.dart' 9 | show ECPublicKey, ECSignature, ECPoint; 10 | import 'package:pointycastle/macs/hmac.dart'; 11 | import "package:pointycastle/signers/ecdsa_signer.dart"; 12 | import 'package:pointycastle/src/utils.dart'; 13 | 14 | import './exception.dart'; 15 | import './key.dart'; 16 | import './key_base.dart'; 17 | 18 | /// EOS Signature 19 | class EOSSignature extends EOSKey { 20 | int? i; 21 | late ECSignature ecSig; 22 | 23 | /// Default constructor from i, r, s 24 | EOSSignature(this.i, BigInt r, BigInt s) { 25 | this.keyType = 'K1'; 26 | this.ecSig = ECSignature(r, s); 27 | } 28 | 29 | /// Construct EOS signature from buffer 30 | EOSSignature.fromBuffer(Uint8List buffer, String? keyType) { 31 | this.keyType = keyType; 32 | 33 | if (buffer.lengthInBytes != 65) { 34 | throw InvalidKey( 35 | 'Invalid signature length, got: ${buffer.lengthInBytes}'); 36 | } 37 | 38 | i = buffer.first; 39 | 40 | if (i! - 27 != i! - 27 & 7) { 41 | throw InvalidKey('Invalid signature parameter'); 42 | } 43 | 44 | BigInt r = decodeBigIntWithSign(1, buffer.sublist(1, 33)); 45 | BigInt s = decodeBigIntWithSign(1, buffer.sublist(33, 65)); 46 | this.ecSig = ECSignature(r, s); 47 | } 48 | 49 | /// Construct EOS signature from string 50 | factory EOSSignature.fromString(String signatureStr) { 51 | RegExp sigRegex = RegExp(r"^SIG_([A-Za-z0-9]+)_([A-Za-z0-9]+)", 52 | caseSensitive: true, multiLine: false); 53 | Iterable match = sigRegex.allMatches(signatureStr); 54 | 55 | if (match.length == 1) { 56 | Match m = match.first; 57 | String? keyType = m.group(1); 58 | Uint8List key = EOSKey.decodeKey(m.group(2)!, keyType); 59 | return EOSSignature.fromBuffer(key, keyType); 60 | } 61 | 62 | throw InvalidKey("Invalid EOS signature"); 63 | } 64 | 65 | /// Verify the signature of the string data 66 | bool verify(String data, EOSPublicKey publicKey) { 67 | Digest d = sha256.convert(utf8.encode(data)); 68 | 69 | return verifyHash(Uint8List.fromList(d.bytes), publicKey); 70 | } 71 | 72 | /// Verify the signature from in SHA256 hashed data 73 | bool verifyHash(Uint8List sha256Data, EOSPublicKey publicKey) { 74 | ECPoint? q = publicKey.q; 75 | final signer = ECDSASigner(null, HMac(SHA256Digest(), 64)); 76 | signer.init(false, PublicKeyParameter(ECPublicKey(q, EOSKey.secp256k1))); 77 | 78 | return signer.verifySignature(sha256Data, this.ecSig); 79 | } 80 | 81 | /** 82 | * Recover the public key used to create this signature using full data. 83 | * @arg {String|Uint8List|List} data - full data 84 | * @return {EOSPublicKey} 85 | */ 86 | EOSPublicKey recover(dynamic data) { 87 | var digest; 88 | 89 | if (data is String) { 90 | var dataBuf = Uint8List.fromList(data.codeUnits); 91 | digest = sha256.convert(dataBuf); 92 | } else if (data is Uint8List || data is List) { 93 | digest = sha256.convert(data); 94 | } else { 95 | throw 'data must be String or uint8list'; 96 | } 97 | 98 | return recoverHash(digest); 99 | } 100 | 101 | /** 102 | * @arg {Digest} dataSha256 - sha256 hash 32 byte buffer 103 | * @return {EOSPublicKey} 104 | */ 105 | EOSPublicKey recoverHash(Digest dataSha256) { 106 | var dataSha256Buf = dataSha256.bytes; 107 | if (dataSha256Buf.length != 32) { 108 | throw "dataSha256: 32 byte String or buffer required"; 109 | } 110 | 111 | var e = decodeBigIntWithSign(1, dataSha256Buf); 112 | var i2 = i!; 113 | i2 -= 27; 114 | i2 = i2 & 3; 115 | 116 | var q = recoverPubKey(e, ecSig, i2); 117 | 118 | return EOSPublicKey.fromPoint(q); 119 | } 120 | 121 | String toString() { 122 | List b = []; 123 | b.add(i!); 124 | b.addAll(encodeBigInt(this.ecSig.r)); 125 | b.addAll(encodeBigInt(this.ecSig.s)); 126 | 127 | Uint8List buffer = Uint8List.fromList(b); 128 | return 'SIG_${keyType}_${EOSKey.encodeKey(buffer, keyType)}'; 129 | } 130 | 131 | /// ECSignature to DER format bytes 132 | static Uint8List ecSigToDER(ECSignature ecSig) { 133 | List r = EOSKey.toSigned(encodeBigInt(ecSig.r)); 134 | List s = EOSKey.toSigned(encodeBigInt(ecSig.s)); 135 | 136 | List b = []; 137 | b.add(0x02); 138 | b.add(r.length); 139 | b.addAll(r); 140 | 141 | b.add(0x02); 142 | b.add(s.length); 143 | b.addAll(s); 144 | 145 | b.insert(0, b.length); 146 | b.insert(0, 0x30); 147 | 148 | return Uint8List.fromList(b); 149 | } 150 | 151 | /// Find the public key recovery factor 152 | static int calcPubKeyRecoveryParam( 153 | BigInt e, ECSignature ecSig, EOSPublicKey publicKey) { 154 | for (int i = 0; i < 4; i++) { 155 | ECPoint? Qprime = recoverPubKey(e, ecSig, i); 156 | if (Qprime == publicKey.q) { 157 | return i; 158 | } 159 | } 160 | throw 'Unable to find valid recovery factor'; 161 | } 162 | 163 | /// Recovery EOS public key from ECSignature 164 | static ECPoint? recoverPubKey(BigInt e, ECSignature ecSig, int i) { 165 | BigInt n = EOSKey.secp256k1.n; 166 | ECPoint G = EOSKey.secp256k1.G; 167 | 168 | BigInt r = ecSig.r; 169 | BigInt s = ecSig.s; 170 | 171 | // A set LSB signifies that the y-coordinate is odd 172 | int isYOdd = i & 1; 173 | 174 | // The more significant bit specifies whether we should use the 175 | // first or second candidate key. 176 | int isSecondKey = i >> 1; 177 | 178 | // 1.1 Let x = r + jn 179 | BigInt x = isSecondKey > 0 ? r + n : r; 180 | ECPoint R = EOSKey.secp256k1.curve.decompressPoint(isYOdd, x); 181 | ECPoint nR = (R * n)!; 182 | if (!nR.isInfinity) { 183 | throw 'nR is not a valid curve point'; 184 | } 185 | 186 | BigInt eNeg = (-e) % n; 187 | BigInt rInv = r.modInverse(n); 188 | 189 | ECPoint? Q = multiplyTwo(R, s, G, eNeg)! * rInv; 190 | return Q; 191 | } 192 | 193 | static bool testBit(BigInt j, int n) { 194 | return (j >> n).toUnsigned(1).toInt() == 1; 195 | } 196 | 197 | static ECPoint? multiplyTwo(ECPoint t, BigInt j, ECPoint x, BigInt k) { 198 | int i = max(j.bitLength, k.bitLength) - 1; 199 | ECPoint? R = t.curve.infinity; 200 | ECPoint? both = t + x; 201 | 202 | while (i >= 0) { 203 | bool jBit = testBit(j, i); 204 | bool kBit = testBit(k, i); 205 | 206 | R = R!.twice(); 207 | 208 | if (jBit) { 209 | if (kBit) { 210 | R = R! + both; 211 | } else { 212 | R = R! + t; 213 | } 214 | } else if (kBit) { 215 | R = R! + x; 216 | } 217 | 218 | --i; 219 | } 220 | 221 | return R; 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: eosdart_ecc 2 | description: Elliptic curve cryptography functions in Dart. Private Key, Public Key, Signature, AES, Encryption, Decryption 3 | version: 0.4.4 4 | homepage: https://github.com/primes-network/eosdart_ecc 5 | 6 | environment: 7 | sdk: ">=2.12.0 <3.0.0" 8 | 9 | dependencies: 10 | bs58check: ^1.0.2 11 | pointycastle: ^3.1.1 12 | crypto: ^3.0.1 13 | 14 | dev_dependencies: 15 | test: ^1.17.7 16 | -------------------------------------------------------------------------------- /test/eosdart_ecc_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:eosdart_ecc/eosdart_ecc.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('EOS Key tests', () { 6 | test('Construct EOS public key from string', () { 7 | EOSPublicKey publicKey = EOSPublicKey.fromString( 8 | 'EOS8Qi58kbERkTJC7A4gabxYU4SbrAxStJHacoke4sf6AvJyEDZXj'); 9 | print(publicKey); 10 | 11 | expect('EOS8Qi58kbERkTJC7A4gabxYU4SbrAxStJHacoke4sf6AvJyEDZXj', 12 | publicKey.toString()); 13 | }); 14 | 15 | test('Construct EOS public key from string PUB_K1 format', () { 16 | EOSPublicKey publicKey = EOSPublicKey.fromString( 17 | 'PUB_K1_859gxfnXyUriMgUeThh1fWv3oqcpLFyHa3TfFYC4PK2Ht7beeX'); 18 | print(publicKey); 19 | }); 20 | 21 | test('Construct EOS private key from string', () { 22 | // common private key 23 | EOSPrivateKey privateKey = EOSPrivateKey.fromString( 24 | '5J9b3xMkbvcT6gYv2EpQ8FD4ZBjgypuNKwE1jxkd7Wd1DYzhk88'); 25 | expect('EOS8Qi58kbERkTJC7A4gabxYU4SbrAxStJHacoke4sf6AvJyEDZXj', 26 | privateKey.toEOSPublicKey().toString()); 27 | expect('5J9b3xMkbvcT6gYv2EpQ8FD4ZBjgypuNKwE1jxkd7Wd1DYzhk88', 28 | privateKey.toString()); 29 | }); 30 | 31 | test('Invalid EOS private key', () { 32 | try { 33 | EOSPrivateKey.fromString( 34 | '5KYZdUEo39z3FPrtuX2QbbwGnNP5zTd7yyr2SC1j299sBCnWjsm'); 35 | fail('Should be invalid private key'); 36 | } on InvalidKey {} catch (e) { 37 | fail('Should throw InvalidKey exception'); 38 | } 39 | }); 40 | 41 | test('Construct random EOS private key from seed', () { 42 | EOSPrivateKey privateKey = EOSPrivateKey.fromSeed('abc'); 43 | print(privateKey); 44 | print(privateKey.toEOSPublicKey()); 45 | 46 | EOSPrivateKey privateKey2 = 47 | EOSPrivateKey.fromString(privateKey.toString()); 48 | expect(privateKey.toEOSPublicKey().toString(), 49 | privateKey2.toEOSPublicKey().toString()); 50 | }); 51 | 52 | test('Construct random EOS private key', () { 53 | EOSPrivateKey privateKey = EOSPrivateKey.fromRandom(); 54 | 55 | print(privateKey); 56 | print(privateKey.toEOSPublicKey()); 57 | 58 | EOSPrivateKey privateKey2 = 59 | EOSPrivateKey.fromString(privateKey.toString()); 60 | expect(privateKey.toEOSPublicKey().toString(), 61 | privateKey2.toEOSPublicKey().toString()); 62 | }); 63 | 64 | test('Construct EOS private key from string in PVT format', () { 65 | // PVT private key 66 | EOSPrivateKey privateKey2 = EOSPrivateKey.fromString( 67 | 'PVT_K1_2jH3nnhxhR3zPUcsKaWWZC9ZmZAnKm3GAnFD1xynGJE1Znuvjd'); 68 | print(privateKey2); 69 | }); 70 | 71 | test('Construct EOS private key from string with compress flag', () { 72 | // Compressed private key 73 | EOSPrivateKey privateKey3 = EOSPrivateKey.fromString( 74 | 'L5TCkLizyYqjvKSy6jg1XM3Lc4uTDwwvHS2BYatyXSyoS8T5kC2z'); 75 | print(privateKey3); 76 | }); 77 | }); 78 | } 79 | -------------------------------------------------------------------------------- /test/sign_string_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:eosdart_ecc/eosdart_ecc.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | EOSPrivateKey privateKey = EOSPrivateKey.fromString( 5 | '5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3'); 6 | EOSPublicKey publicKey = privateKey.toEOSPublicKey(); 7 | 8 | bool eccSignAndVerify(String data) { 9 | privateKey.signString(data).toString(); 10 | EOSSignature signature = privateKey.signString(data); 11 | return signature.verify(data, publicKey); 12 | } 13 | 14 | void main() { 15 | group('EOS signature string tests', () { 16 | test('ECC Sign #1', () { 17 | var result = eccSignAndVerify('data'); 18 | expect(result, true); 19 | }); 20 | 21 | test('ECC Sign #2', () { 22 | var result = eccSignAndVerify( 23 | '02dd11a0cd4ec12119e2e72d1a553e657323435e08063ac511d8b7f52802cf9c'); 24 | // Error: Value x must be smaller than q 25 | expect(result, true); 26 | }); 27 | 28 | test('ECC Sign #3', () { 29 | var result = eccSignAndVerify('475'); 30 | // Error: Value x must be smaller than q 31 | expect(result, true); 32 | }); 33 | 34 | test('ECC Sign #4', () { 35 | var result = eccSignAndVerify('5488'); 36 | // Error: Invalid point compression 37 | expect(result, true); 38 | }); 39 | 40 | test('ECC Sign #5', () { 41 | var result = eccSignAndVerify('2097'); 42 | // Error: nR is not a valid curve point 43 | expect(result, true); 44 | }); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /test/signature_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:eosdart_ecc/eosdart_ecc.dart'; 5 | import 'package:test/test.dart'; 6 | import 'package:crypto/crypto.dart'; 7 | 8 | void main() { 9 | group('EOS signature tests', () { 10 | test('Construct EOS signature from string', () { 11 | String sigStr = 12 | 'SIG_K1_Kg417TSLuhzSpU2bGa21kD1UNaTfAZSCcKmKpZ6fnx3Nqu22gzG3ND4Twur7bzX8oS1J91JvV4rMJcFycGqFBSaY2SJcEQ'; 13 | EOSSignature signature = EOSSignature.fromString(sigStr); 14 | print(signature); 15 | 16 | expect(sigStr, signature.toString()); 17 | }); 18 | 19 | test('Sign the hash using private key', () { 20 | EOSPrivateKey privateKey = EOSPrivateKey.fromString( 21 | '5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3'); 22 | EOSPublicKey publicKey = privateKey.toEOSPublicKey(); 23 | String expectedSig = 24 | 'SIG_K1_Kg417TSLuhzSpU2bGa21kD1UNaTfAZSCcKmKpZ6fnx3Nqu22gzG3ND4Twur7bzX8oS1J91JvV4rMJcFycGqFBSaY2SJcEQ'; 25 | 26 | String data = 'data'; 27 | Uint8List hashData = sha256.convert(utf8.encode(data)).bytes as Uint8List; 28 | EOSSignature signature = privateKey.signHash(hashData); 29 | EOSSignature signature2 = privateKey.signString(data); 30 | 31 | print(signature.toString()); 32 | expect(expectedSig, signature.toString()); 33 | expect(true, signature.verifyHash(hashData, publicKey)); 34 | expect(true, signature2.verifyHash(hashData, publicKey)); 35 | 36 | expect(true, signature.verify(data, publicKey)); 37 | expect(true, signature2.verify(data, publicKey)); 38 | }); 39 | 40 | test('Sign the hash using private key', () { 41 | EOSPrivateKey privateKey = EOSPrivateKey.fromString( 42 | '5HxT6prWB8VuXkoAaX3eby8bWjquMtCvGuakhC8tGEiPSHfsQLR'); 43 | EOSPublicKey publicKey = privateKey.toEOSPublicKey(); 44 | String expectedSig = 45 | 'SIG_K1_Kdfe9wknSAKBmgwb3L53CG8KosoHhZ69oVEJrrH5YuWx4JVcJdn1ZV3MU25AVho4mPbeSKW79DVTBAAWj7zGbHTByF1JXU'; 46 | 47 | List l = [ 48 | 244, 49 | 163, 50 | 240, 51 | 75, 52 | 174, 53 | 150, 54 | 233, 55 | 185, 56 | 227, 57 | 66, 58 | 27, 59 | 130, 60 | 230, 61 | 139, 62 | 102, 63 | 112, 64 | 128, 65 | 38, 66 | 78, 67 | 233, 68 | 105, 69 | 59, 70 | 61, 71 | 11, 72 | 25, 73 | 221, 74 | 42, 75 | 109, 76 | 80, 77 | 184, 78 | 174, 79 | 201 80 | ]; 81 | Uint8List hashData = Uint8List.fromList(l); 82 | EOSSignature signature = privateKey.signHash(hashData); 83 | 84 | expect(expectedSig, signature.toString()); 85 | print(signature.toString()); 86 | expect(true, signature.verifyHash(hashData, publicKey)); 87 | }); 88 | 89 | test('Sign the hash using private key', () { 90 | EOSPrivateKey privateKey = EOSPrivateKey.fromString( 91 | '5J9b3xMkbvcT6gYv2EpQ8FD4ZBjgypuNKwE1jxkd7Wd1DYzhk88'); 92 | EOSPublicKey publicKey = privateKey.toEOSPublicKey(); 93 | String expectedSig = 94 | 'SIG_K1_KWfDGxwogny1PUiBAYTfKwPsCSNvM7zWgmXyChdYayZFfyPjddpBUYVdJTq1PjC3PRXADRsqWVU1N2SMQivBDqA7AaRzmB'; 95 | 96 | List l = [ 97 | 136, 98 | 139, 99 | 63, 100 | 11, 101 | 114, 102 | 68, 103 | 227, 104 | 116, 105 | 92, 106 | 61, 107 | 64, 108 | 121, 109 | 147, 110 | 210, 111 | 233, 112 | 25, 113 | 74, 114 | 164, 115 | 140, 116 | 112, 117 | 45, 118 | 5, 119 | 254, 120 | 165, 121 | 208, 122 | 158, 123 | 53, 124 | 212, 125 | 128, 126 | 190, 127 | 153, 128 | 142 129 | ]; 130 | Uint8List hashData = Uint8List.fromList(l); 131 | EOSSignature signature = privateKey.signHash(hashData); 132 | 133 | expect(expectedSig, signature.toString()); 134 | print(signature.toString()); 135 | expect(true, signature.verifyHash(hashData, publicKey)); 136 | }); 137 | 138 | test('Recover EOSPublicKey from sign data', () { 139 | const data = 'this is some data to sign'; 140 | 141 | var eosPrivateKey = EOSPrivateKey.fromRandom(); 142 | var eosPublicKey = eosPrivateKey.toEOSPublicKey(); 143 | 144 | var signature = eosPrivateKey.signString(data); 145 | 146 | var recoveredEOSPublicKey = signature.recover(data); 147 | 148 | expect(eosPublicKey.toString(), recoveredEOSPublicKey.toString()); 149 | print('Generated EOSPublicKey : ${eosPublicKey.toString()}'); 150 | print('Recovered EOSPublicKey : ${recoveredEOSPublicKey.toString()}'); 151 | }); 152 | }); 153 | } 154 | --------------------------------------------------------------------------------