Skip to content

Commit e5110d6

Browse files
Initial Commit
0 parents  commit e5110d6

30 files changed

+21851
-0
lines changed

.gitignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# https://dart.dev/guides/libraries/private-files
2+
# Created by `dart pub`
3+
.dart_tool/
4+
5+
# Avoid committing pubspec.lock for library packages; see
6+
# https://dart.dev/guides/libraries/private-files#pubspeclock.
7+
pubspec.lock
8+
9+
# InteliJ
10+
.idea/
11+
*.iml

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
## 1.0.0
2+
3+
- Initial version.

README.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Polyseed
2+
3+
A dart implementation of [polyseed](https://github.com/tevador/polyseed).
4+
5+
## Features
6+
7+
* 16 mnemonic words (36% shorter than the original 25-word seed)
8+
* embedded wallet birthday to optimize restoring from the seed
9+
* supports encryption by a passphrase
10+
* can store up to 3 custom bits
11+
* advanced checksum based on a polynomial code
12+
* seeds are incompatible between different coins
13+
14+
Supported languages:
15+
16+
1. English
17+
2. Japanese
18+
3. Korean
19+
4. Spanish
20+
5. French
21+
6. Italian
22+
7. Czech
23+
8. Portuguese
24+
9. Chinese (Simplified)
25+
10. Chinese (Traditional)
26+
27+
For languages based on the latin alphabet, just the first 4 characters of each word need to be provided when restoring a
28+
seed. French and Spanish seeds can be input with or without accents. Wordlists are based
29+
on [BIP-39](https://github.com/bitcoin/bips/blob/master/bip-0039/bip-0039-wordlists.md) with a few minor changes.
30+
31+
## Encoding
32+
33+
Each word contains 11 bits of information. The data are encoded as follows:
34+
35+
| word # | contents |
36+
|--------|------------------------------------------|
37+
| 1 | checksum (11 bits) |
38+
| 2-6 | secret seed (10 bits) + features (1 bit) |
39+
| 7-16 | secret seed (10 bits) + birthday (1 bit) |
40+
41+
In total, there are 11 bits for the checksum, 150 bits for the secret seed, 5 feature bits and 10 birthday bits. Because
42+
the feature and birthday bits are non-random, they are spread over the 15 data words so that two different mnemonic
43+
phrases are unlikely to have the same word in the same position.
44+
45+
### Checksum
46+
47+
The mnemonic phrase can be treated as a polynomial over GF(2048), which enables the use of an efficient Reed-Solomon
48+
error correction code with one check word. All single-word errors can be detected and all single-word erasures can be
49+
corrected without false positives.
50+
51+
To prevent the seed from being accidentally used with a different cryptocurrency, a coin flag is XORed with the second
52+
word after the checksum is calculated. Checksum validation will fail unless the wallet software XORs the same coin flag
53+
with the second word when restoring.
54+
55+
### Feature bits
56+
57+
There are 5 feature bits in the phrase. The first 2 bits are for internal use (one bit is used to indicate a seed
58+
encrypted by a passphrase and the other bit is reserved for a future update of the key derivation function). The
59+
remaining 3 bits are reserved for library users and can be enabled and accessed through the API. The library requires
60+
reserved bits to be zero (if not, `UnsupportedSeedFeatureException` is thrown).
61+
62+
### Wallet birthday
63+
64+
The mnemonic phrase stores the approximate date when the wallet was created. This allows the seed to be generated
65+
offline without access to the blockchain. Wallet software can easily convert a date to the corresponding block height
66+
when restoring a seed.
67+
68+
The wallet birthday has a resolution of 2629746 seconds (1/12 of the average Gregorian year). All dates between November
69+
2021 and February 2107 can be represented.
70+
71+
### Secret seed
72+
73+
Polyseed was designed for the 128-bit security level. This corresponds to the security of the ed25519 elliptic curve,
74+
which requires [about 2<sup>126</sup> operations](https://safecurves.cr.yp.to/rho.html) to break a key.
75+
76+
The private key is derived from the 150-bit secret seed using PBKDF2-HMAC-SHA256 with 10000 iterations. The KDF
77+
parameters were selected to allow for the key to be derived by hardware wallets. Key generation is domain-separated by
78+
the wallet birthday month, seed features and the coin flag.
79+
80+
The size of the secret seed and the domain separation parameters provide a comfortable security margin
81+
against [multi-target attacks](https://blog.cr.yp.to/20151120-batchattacks.html).

analysis_options.yaml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# This file configures the static analysis results for your project (errors,
2+
# warnings, and lints).
3+
#
4+
# This enables the 'recommended' set of lints from `package:lints`.
5+
# This set helps identify many issues that may lead to problems when running
6+
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
7+
# style and format.
8+
#
9+
# If you want a smaller set of lints you can change this to specify
10+
# 'package:lints/core.yaml'. These are just the most critical lints
11+
# (the recommended set includes the core lints).
12+
# The core lints are also what is used by pub.dev for scoring packages.
13+
14+
include: package:lints/recommended.yaml
15+
16+
# Uncomment the following section to specify additional rules.
17+
18+
# linter:
19+
# rules:
20+
# - camel_case_types
21+
22+
# analyzer:
23+
# exclude:
24+
# - path/to/excluded/files/**
25+
26+
# For more information about the core and recommended set of lints, see
27+
# https://dart.dev/go/core-lints
28+
29+
# For additional information about configuring this file, see
30+
# https://dart.dev/guides/language/analysis-options

example/polyseed_dart_example.dart

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import 'package:polyseed/polyseed_dart.dart';
2+
import 'package:polyseed/src/mnemonics/en_lang.dart';
3+
import 'package:polyseed/src/polyseed_coin.dart';
4+
import 'package:polyseed/src/utils/key_utils.dart';
5+
6+
void main() {
7+
var seed = polyseedCreate(0);
8+
var seedStr = polyseed_encode(seed, EnLang(), PolyseedCoin.POLYSEED_MONERO);
9+
print('Seed: $seedStr');
10+
11+
var seed2 = polyseedDecode(seedStr, EnLang(), PolyseedCoin.POLYSEED_MONERO);
12+
print(
13+
'seed2: ${polyseed_encode(seed2, EnLang(), PolyseedCoin.POLYSEED_MONERO)}');
14+
15+
var seedEnc = polyseed_crypt(seed, "CakeWallet");
16+
print(polyseed_is_encrypted(seedEnc));
17+
print(
18+
'seedEnc: ${polyseed_encode(seedEnc, EnLang(), PolyseedCoin.POLYSEED_MONERO)}');
19+
20+
21+
var seedDec = polyseed_crypt(seedEnc, "CakeWallet");
22+
print(polyseed_is_encrypted(seedDec));
23+
print(
24+
'seedEnc: ${polyseed_encode(seedDec, EnLang(), PolyseedCoin.POLYSEED_MONERO)}');
25+
26+
var seedFeatherStr = "unaware yard donate shallow slot sing oil oxygen loyal bench near hill surround forum execute lamp";
27+
var seedFeather = polyseedDecode(seedFeatherStr, EnLang(), PolyseedCoin.POLYSEED_MONERO);
28+
29+
var key = polyseed_keygen(seedFeather, PolyseedCoin.POLYSEED_MONERO, 32);
30+
print(polyseed_get_birthday(seedFeather));
31+
print(keyToHexString(key));
32+
}
33+
34+

lib/polyseed_dart.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/// Support for doing something awesome.
2+
///
3+
/// More dartdocs go here.
4+
library;
5+
6+
export 'src/polyseed.dart';
7+
8+
// TODO: Export any libraries intended for clients of this package.

lib/src/birthday.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
const EPOCH = 1635768000; /* 1st November 2021 12:00 UTC */
2+
const TIME_STEP = 2629746; /* 30.436875 days = 1/12 of the Gregorian year */
3+
4+
const DATE_BITS = 10;
5+
const DATE_MASK = (1 << DATE_BITS) - 1;
6+
7+
8+
int birthdayEncode(int time) {
9+
if (time == -1 || time < EPOCH) {
10+
return 0;
11+
}
12+
return ((time - EPOCH) / TIME_STEP).floor() & DATE_MASK;
13+
}
14+
15+
int birthdayDecode(int birthday) => EPOCH + birthday * TIME_STEP;

lib/src/features.dart

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
const int FEATURE_BITS = 5;
2+
const int FEATURE_MASK = (1 << FEATURE_BITS) - 1;
3+
const int INTERNAL_FEATURES = 2;
4+
const int USER_FEATURES = 3;
5+
const int USER_FEATURES_MASK = (1 << USER_FEATURES) - 1;
6+
const int ENCRYPTED_MASK = 16;
7+
int reservedFeatures = FEATURE_MASK ^ ENCRYPTED_MASK;
8+
9+
int makeFeatures(int userFeatures) => userFeatures & USER_FEATURES_MASK;
10+
11+
int getFeatures(int features, int mask) =>
12+
features & (mask & USER_FEATURES_MASK);
13+
14+
bool isEncrypted(int features) => (features & ENCRYPTED_MASK) != 0;
15+
16+
bool polyseedFeaturesSupported(int features) =>
17+
(features & reservedFeatures) == 0;
18+
19+
int polyseedEnableFeatures(int mask) {
20+
int numEnabled = 0;
21+
reservedFeatures = FEATURE_MASK ^ ENCRYPTED_MASK;
22+
for (int i = 0; i < USER_FEATURES; ++i) {
23+
int fmask = 1 << i;
24+
if ((mask & fmask) != 0) {
25+
reservedFeatures ^= fmask;
26+
numEnabled++;
27+
}
28+
}
29+
return numEnabled;
30+
}

lib/src/gf.dart

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import 'dart:typed_data';
2+
3+
import 'package:polyseed/src/birthday.dart';
4+
import 'package:polyseed/src/features.dart';
5+
import 'package:polyseed/src/polyseed_data.dart';
6+
7+
import 'polyseed.dart';
8+
import 'dart:math';
9+
10+
const int CHAR_BIT = 8;
11+
const int SECRET_BITS = 150;
12+
const int SECRET_BUFFER_SIZE = 32;
13+
14+
const int GF_BITS = 11;
15+
const int GF_SIZE = 1 << GF_BITS;
16+
const int GF_MASK = GF_SIZE - 1;
17+
const int POLY_NUM_CHECK_DIGITS = 1;
18+
19+
const int SHARE_BITS = 10; /* bits of the secret per word */
20+
const int DATA_WORDS = POLYSEED_NUM_WORDS - POLY_NUM_CHECK_DIGITS;
21+
22+
List<int> polyseed_mul2_table = [5, 7, 1, 3, 13, 15, 9, 11];
23+
// POLYSEED_PRIVATE gf_elem polyseed_mul2_table[8] = {
24+
// 5, 7, 1, 3, 13, 15, 9, 11
25+
// };
26+
27+
class GFPoly {
28+
List<int> coeff = Uint16List(POLYSEED_NUM_WORDS);
29+
}
30+
31+
int gf_elem_mul2(int x) {
32+
if (x < 1024) {
33+
return 2 * x;
34+
}
35+
return polyseed_mul2_table[x % 8] + 16 * ((x - 1024) ~/ 8);
36+
}
37+
38+
int gf_poly_eval(GFPoly poly) {
39+
int result = poly.coeff[POLYSEED_NUM_WORDS - 1];
40+
for (int i = POLYSEED_NUM_WORDS - 2; i >= 0; --i) {
41+
result = gf_elem_mul2(result) ^ poly.coeff[i];
42+
}
43+
return result;
44+
}
45+
46+
GFPoly gf_poly_encode(GFPoly message) {
47+
message.coeff[0] = gf_poly_eval(message);
48+
return message;
49+
}
50+
51+
bool gf_poly_check(GFPoly message) => gf_poly_eval(message) == 0;
52+
53+
GFPoly polyseed_data_to_poly(PolyseedData data, GFPoly poly) {
54+
int extraVal = (data.features << DATE_BITS) | data.birthday;
55+
int extraBits = FEATURE_BITS + DATE_BITS;
56+
57+
int wordBits = 0;
58+
int wordVal = 0;
59+
60+
int secretIdx = 0;
61+
int secretVal = data.secret[secretIdx];
62+
int secretBits = CHAR_BIT;
63+
int seedRemBits = SECRET_BITS - CHAR_BIT;
64+
65+
for (int i = 0; i < DATA_WORDS; ++i) {
66+
while (wordBits < SHARE_BITS) {
67+
if (secretBits == 0) {
68+
secretIdx++;
69+
secretBits = min(seedRemBits, CHAR_BIT);
70+
secretVal = data.secret[secretIdx];
71+
seedRemBits -= secretBits;
72+
}
73+
74+
int chunkBits = min(secretBits, SHARE_BITS - wordBits);
75+
secretBits -= chunkBits;
76+
wordBits += chunkBits;
77+
wordVal <<= chunkBits;
78+
wordVal |= (secretVal >> secretBits) & ((1 << chunkBits) - 1);
79+
}
80+
wordVal <<= 1;
81+
extraBits--;
82+
wordVal |= (extraVal >> extraBits) & 1;
83+
poly.coeff[POLY_NUM_CHECK_DIGITS + i] = wordVal;
84+
wordVal = 0;
85+
wordBits = 0;
86+
}
87+
88+
assert(seedRemBits == 0);
89+
assert(secretBits == 0);
90+
assert(extraBits == 0);
91+
92+
return poly;
93+
}
94+
95+
PolyseedData polyseed_poly_to_data(GFPoly poly) {
96+
int birthday = 0;
97+
int features = 0;
98+
Uint8List secret = Uint8List(SECRET_BUFFER_SIZE);
99+
int checksum = poly.coeff[0];
100+
101+
int extraVal = 0;
102+
int extraBits = 0;
103+
104+
int wordBits = 0;
105+
int wordVal = 0;
106+
107+
int secretIdx = 0;
108+
int secretBits = 0;
109+
int seedBits = 0;
110+
111+
for (int i = POLY_NUM_CHECK_DIGITS; i < POLYSEED_NUM_WORDS; ++i) {
112+
wordVal = poly.coeff[i];
113+
114+
extraVal <<= 1;
115+
extraVal |= wordVal & 1;
116+
wordVal >>= 1;
117+
wordBits = GF_BITS - 1;
118+
extraBits++;
119+
120+
while (wordBits > 0) {
121+
if (secretBits == CHAR_BIT) {
122+
secretIdx++;
123+
seedBits += secretBits;
124+
secretBits = 0;
125+
}
126+
127+
int chunkBits = min(wordBits, CHAR_BIT - secretBits);
128+
wordBits -= chunkBits;
129+
int chunkMask = ((1 << chunkBits) - 1);
130+
if (chunkBits < CHAR_BIT) {
131+
secret[secretIdx] <<= chunkBits;
132+
}
133+
secret[secretIdx] |= (wordVal >> wordBits) & chunkMask;
134+
secretBits += chunkBits;
135+
}
136+
}
137+
138+
seedBits += secretBits;
139+
140+
assert(wordBits == 0);
141+
assert(seedBits == SECRET_BITS);
142+
assert(extraBits == FEATURE_BITS + DATE_BITS);
143+
144+
birthday = extraVal & DATE_MASK;
145+
features = extraVal >> DATE_BITS;
146+
147+
return PolyseedData(birthday: birthday,
148+
features: features,
149+
secret: secret,
150+
checksum: checksum);
151+
}

0 commit comments

Comments
 (0)