@bcts/seedtool-cli 1.0.0-alpha.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +48 -0
- package/README.md +11 -0
- package/dist/index.cjs +1370 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +654 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +654 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1304 -0
- package/dist/index.mjs.map +1 -0
- package/dist/main.mjs +1375 -0
- package/dist/main.mjs.map +1 -0
- package/package.json +95 -0
- package/src/cli.ts +345 -0
- package/src/formats/base10.ts +39 -0
- package/src/formats/base6.ts +39 -0
- package/src/formats/bip39.ts +43 -0
- package/src/formats/bits.ts +39 -0
- package/src/formats/bytewords-minimal.ts +35 -0
- package/src/formats/bytewords-standard.ts +35 -0
- package/src/formats/bytewords-uri.ts +35 -0
- package/src/formats/cards.ts +100 -0
- package/src/formats/dice.ts +39 -0
- package/src/formats/envelope.ts +34 -0
- package/src/formats/format.ts +145 -0
- package/src/formats/hex.ts +34 -0
- package/src/formats/index.ts +31 -0
- package/src/formats/ints.ts +37 -0
- package/src/formats/multipart.ts +64 -0
- package/src/formats/random.ts +28 -0
- package/src/formats/seed-format.ts +37 -0
- package/src/formats/sskr.ts +290 -0
- package/src/index.ts +44 -0
- package/src/main.ts +227 -0
- package/src/random.ts +130 -0
- package/src/seed.ts +300 -0
- package/src/styles.ts +50 -0
- package/src/util.ts +140 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multipart UR format
|
|
3
|
+
* Ported from seedtool-cli-rust/src/formats/multipart.rs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Cli } from "../cli.js";
|
|
7
|
+
import type { InputFormat, OutputFormat } from "./format.js";
|
|
8
|
+
import { Seed } from "../seed.js";
|
|
9
|
+
import { Envelope } from "@bcts/envelope";
|
|
10
|
+
import { MultipartEncoder, MultipartDecoder } from "@bcts/uniform-resources";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Multipart UR format handler.
|
|
14
|
+
* Round-trippable: multipart URs → seed → multipart URs.
|
|
15
|
+
* Uses fountain encoding for reliable transmission.
|
|
16
|
+
*/
|
|
17
|
+
export class MultipartFormat implements InputFormat, OutputFormat {
|
|
18
|
+
name(): string {
|
|
19
|
+
return "multipart";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
roundTrippable(): boolean {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
processInput(state: Cli): Cli {
|
|
27
|
+
const input = state.expectInput();
|
|
28
|
+
const shares = input.split(/\s+/).filter((s) => s.length > 0);
|
|
29
|
+
|
|
30
|
+
const decoder = new MultipartDecoder();
|
|
31
|
+
for (const share of shares) {
|
|
32
|
+
decoder.receive(share);
|
|
33
|
+
if (decoder.isComplete()) {
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!decoder.isComplete()) {
|
|
39
|
+
throw new Error("Insufficient multipart shares");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const ur = decoder.message();
|
|
43
|
+
if (ur === undefined) {
|
|
44
|
+
throw new Error("Failed to decode multipart message");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const envelope = Envelope.fromUR(ur);
|
|
48
|
+
state.seed = Seed.fromEnvelope(envelope);
|
|
49
|
+
return state;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
processOutput(state: Cli): string {
|
|
53
|
+
const ur = state.toEnvelope().ur();
|
|
54
|
+
const encoder = new MultipartEncoder(ur, state.maxFragmentLen);
|
|
55
|
+
const partsCount = encoder.partsCount() + state.additionalParts;
|
|
56
|
+
|
|
57
|
+
const parts: string[] = [];
|
|
58
|
+
for (let i = 0; i < partsCount; i++) {
|
|
59
|
+
parts.push(encoder.nextPart());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return parts.join("\n");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Random format
|
|
3
|
+
* Ported from seedtool-cli-rust/src/formats/random.rs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Cli } from "../cli.js";
|
|
7
|
+
import type { InputFormat } from "./format.js";
|
|
8
|
+
import { Seed } from "../seed.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Random seed generation format.
|
|
12
|
+
* Input-only: generates a new random seed.
|
|
13
|
+
*/
|
|
14
|
+
export class RandomFormat implements InputFormat {
|
|
15
|
+
name(): string {
|
|
16
|
+
return "random";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
roundTrippable(): boolean {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
processInput(state: Cli): Cli {
|
|
24
|
+
const data = state.randomData(state.count);
|
|
25
|
+
state.seed = Seed.new(data);
|
|
26
|
+
return state;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seed UR format
|
|
3
|
+
* Ported from seedtool-cli-rust/src/formats/seed.rs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Cli } from "../cli.js";
|
|
7
|
+
import type { InputFormat, OutputFormat } from "./format.js";
|
|
8
|
+
import { Seed } from "../seed.js";
|
|
9
|
+
import { Seed as ComponentsSeed } from "@bcts/components";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Seed UR format handler.
|
|
13
|
+
* Round-trippable: seed UR → seed → seed UR.
|
|
14
|
+
* Uses the ur:seed format from bc-components.
|
|
15
|
+
*/
|
|
16
|
+
export class SeedFormat implements InputFormat, OutputFormat {
|
|
17
|
+
name(): string {
|
|
18
|
+
return "seed";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
roundTrippable(): boolean {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
processInput(state: Cli): Cli {
|
|
26
|
+
const input = state.expectInput();
|
|
27
|
+
const componentsSeed = ComponentsSeed.fromURString(input);
|
|
28
|
+
state.seed = Seed.fromComponentsSeed(componentsSeed);
|
|
29
|
+
return state;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
processOutput(state: Cli): string {
|
|
33
|
+
const seed = state.seedWithOverrides();
|
|
34
|
+
const componentsSeed = seed.toComponentsSeed();
|
|
35
|
+
return componentsSeed.urString();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSKR format
|
|
3
|
+
* Ported from seedtool-cli-rust/src/formats/sskr.rs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Cli, SSKRFormatKey } from "../cli.js";
|
|
7
|
+
import type { InputFormat, OutputFormat } from "./format.js";
|
|
8
|
+
import { Seed } from "../seed.js";
|
|
9
|
+
import { Envelope } from "@bcts/envelope";
|
|
10
|
+
import { SymmetricKey } from "@bcts/envelope";
|
|
11
|
+
import { SSKRShare, SSKRShareCbor, SSKRSecret, sskrGenerate, sskrCombine } from "@bcts/components";
|
|
12
|
+
import { encodeBytewords, decodeBytewords, BytewordsStyle, UR } from "@bcts/uniform-resources";
|
|
13
|
+
import { SSKR_SHARE, SSKR_SHARE_V1 } from "@bcts/tags";
|
|
14
|
+
import { toByteString, toTaggedValue, decodeCbor, expectBytes } from "@bcts/dcbor";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* SSKR format handler.
|
|
18
|
+
* Round-trippable: sskr shares → seed → sskr shares.
|
|
19
|
+
* Supports multiple sub-formats: envelope, btw, btwm, btwu, ur.
|
|
20
|
+
*/
|
|
21
|
+
export class SSKRFormat implements InputFormat, OutputFormat {
|
|
22
|
+
name(): string {
|
|
23
|
+
return "sskr";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
roundTrippable(): boolean {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
processInput(state: Cli): Cli {
|
|
31
|
+
const input = state.expectInput();
|
|
32
|
+
state.seed = parseSskrSeed(input);
|
|
33
|
+
return state;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
processOutput(state: Cli): string {
|
|
37
|
+
const spec = state.sskrSpec();
|
|
38
|
+
const seed = state.expectSeed();
|
|
39
|
+
const format = state.sskrFormat;
|
|
40
|
+
return outputSskrSeed(seed, spec, format);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
//
|
|
45
|
+
// Output Helpers
|
|
46
|
+
//
|
|
47
|
+
|
|
48
|
+
function outputSskrSeed(
|
|
49
|
+
seed: Seed,
|
|
50
|
+
spec: import("@bcts/sskr").Spec,
|
|
51
|
+
format: SSKRFormatKey,
|
|
52
|
+
): string {
|
|
53
|
+
switch (format) {
|
|
54
|
+
case "envelope": {
|
|
55
|
+
const envelope = seed.toEnvelope();
|
|
56
|
+
const contentKey = SymmetricKey.new();
|
|
57
|
+
const encryptedEnvelope = envelope.wrap().encryptSubject(contentKey);
|
|
58
|
+
const shareEnvelopes = encryptedEnvelope.sskrSplitFlattened(spec, contentKey);
|
|
59
|
+
const shareEnvelopesStrings = shareEnvelopes.map((envelope) => envelope.urString());
|
|
60
|
+
return shareEnvelopesStrings.join("\n");
|
|
61
|
+
}
|
|
62
|
+
case "btw": {
|
|
63
|
+
return makeBytewordsShares(spec, seed, BytewordsStyle.Standard);
|
|
64
|
+
}
|
|
65
|
+
case "btwm": {
|
|
66
|
+
return makeBytewordsShares(spec, seed, BytewordsStyle.Minimal);
|
|
67
|
+
}
|
|
68
|
+
case "btwu": {
|
|
69
|
+
return makeBytewordsShares(spec, seed, BytewordsStyle.Uri);
|
|
70
|
+
}
|
|
71
|
+
case "ur": {
|
|
72
|
+
const shares = makeShares(spec, seed);
|
|
73
|
+
const urStrings = shares.map((share) => {
|
|
74
|
+
const ur = UR.fromCbor("sskr", toByteString(share.asBytes()));
|
|
75
|
+
return ur.toString();
|
|
76
|
+
});
|
|
77
|
+
return urStrings.join("\n");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function makeShares(spec: import("@bcts/sskr").Spec, seed: Seed): SSKRShareCbor[] {
|
|
83
|
+
const secret = SSKRSecret.new(seed.data());
|
|
84
|
+
const shareGroups = sskrGenerate(spec, secret);
|
|
85
|
+
const flatShares: SSKRShareCbor[] = [];
|
|
86
|
+
for (const group of shareGroups) {
|
|
87
|
+
for (const shareData of group) {
|
|
88
|
+
flatShares.push(SSKRShare.fromData(shareData));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return flatShares;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function makeBytewordsShares(
|
|
95
|
+
spec: import("@bcts/sskr").Spec,
|
|
96
|
+
seed: Seed,
|
|
97
|
+
style: BytewordsStyle,
|
|
98
|
+
): string {
|
|
99
|
+
const shares = makeShares(spec, seed);
|
|
100
|
+
const cborShares = shares.map((share) =>
|
|
101
|
+
toTaggedValue(SSKR_SHARE.value, toByteString(share.asBytes())),
|
|
102
|
+
);
|
|
103
|
+
const sharesStrings = cborShares.map((share) => encodeBytewords(share.toData(), style));
|
|
104
|
+
return sharesStrings.join("\n");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
//
|
|
108
|
+
// Input Helpers
|
|
109
|
+
//
|
|
110
|
+
|
|
111
|
+
function parseEnvelopes(input: string): Seed | null {
|
|
112
|
+
try {
|
|
113
|
+
const shareStrings = input.split(/\s+/).filter((s) => s.length > 0);
|
|
114
|
+
const shareEnvelopes: Envelope[] = [];
|
|
115
|
+
for (const str of shareStrings) {
|
|
116
|
+
try {
|
|
117
|
+
const envelope = Envelope.fromURString(str);
|
|
118
|
+
shareEnvelopes.push(envelope);
|
|
119
|
+
} catch {
|
|
120
|
+
// Skip non-envelope strings
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (shareEnvelopes.length === 0) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const recoveredEnvelope = Envelope.sskrJoin(shareEnvelopes).unwrap();
|
|
129
|
+
return Seed.fromEnvelope(recoveredEnvelope);
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function fromUntaggedCborShares(untaggedCborShares: Uint8Array[]): Seed | null {
|
|
136
|
+
try {
|
|
137
|
+
const recoveredSecret = sskrCombine(untaggedCborShares);
|
|
138
|
+
return Seed.new(recoveredSecret.getData());
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function fromTaggedCborShares(taggedCborDataShares: Uint8Array[]): Seed | null {
|
|
145
|
+
try {
|
|
146
|
+
const untaggedShares: Uint8Array[] = [];
|
|
147
|
+
for (const data of taggedCborDataShares) {
|
|
148
|
+
const cbor = decodeCbor(data);
|
|
149
|
+
// Extract the tagged value and ensure it's the expected tag
|
|
150
|
+
const tagged = cbor as { tag?: number; value?: unknown };
|
|
151
|
+
if (tagged.tag !== SSKR_SHARE.value && tagged.tag !== SSKR_SHARE_V1.value) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
const content = (tagged.value as { buffer?: Uint8Array }) || cbor;
|
|
155
|
+
const bytes = expectBytes(content as ReturnType<typeof decodeCbor>);
|
|
156
|
+
untaggedShares.push(bytes);
|
|
157
|
+
}
|
|
158
|
+
return fromUntaggedCborShares(untaggedShares);
|
|
159
|
+
} catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function parseBytewords(input: string, style: BytewordsStyle): Seed | null {
|
|
165
|
+
try {
|
|
166
|
+
// Standard bytewords include spaces, so we can only split on newlines.
|
|
167
|
+
let shareStrings: string[];
|
|
168
|
+
if (style === BytewordsStyle.Standard) {
|
|
169
|
+
shareStrings = input.split("\n").filter((s) => s.length > 0);
|
|
170
|
+
} else {
|
|
171
|
+
shareStrings = input.split(/\s+/).filter((s) => s.length > 0);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const cborDataShares: Uint8Array[] = [];
|
|
175
|
+
for (const s of shareStrings) {
|
|
176
|
+
try {
|
|
177
|
+
const decoded = decodeBytewords(s, style);
|
|
178
|
+
cborDataShares.push(decoded);
|
|
179
|
+
} catch {
|
|
180
|
+
// Skip invalid bytewords
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (cborDataShares.length === 0) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return fromTaggedCborShares(cborDataShares);
|
|
189
|
+
} catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function parseUr(input: string, expectedTagValue: number, allowTaggedCbor: boolean): Seed | null {
|
|
195
|
+
try {
|
|
196
|
+
// Get the expected type name
|
|
197
|
+
const expectedType = expectedTagValue === SSKR_SHARE.value ? "sskr" : "crypto-sskr";
|
|
198
|
+
|
|
199
|
+
const shareStrings = input.split(/\s+/).filter((s) => s.length > 0);
|
|
200
|
+
const urs: UR[] = [];
|
|
201
|
+
|
|
202
|
+
for (const str of shareStrings) {
|
|
203
|
+
try {
|
|
204
|
+
const ur = UR.fromURString(str);
|
|
205
|
+
urs.push(ur);
|
|
206
|
+
} catch {
|
|
207
|
+
// Skip non-UR strings
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (urs.length === 0) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Ensure every UR is of the expected type
|
|
216
|
+
for (const ur of urs) {
|
|
217
|
+
if (ur.type() !== expectedType) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const untaggedCborShares: Uint8Array[] = [];
|
|
223
|
+
for (const ur of urs) {
|
|
224
|
+
let cbor = ur.cbor();
|
|
225
|
+
|
|
226
|
+
// Legacy SSKR shares might have tagged CBOR, even though they're
|
|
227
|
+
// URs so they shouldn't be.
|
|
228
|
+
if (allowTaggedCbor) {
|
|
229
|
+
try {
|
|
230
|
+
const decoded = decodeCbor(cbor.toData());
|
|
231
|
+
const tagged = decoded as { tag?: number; value?: unknown };
|
|
232
|
+
if (tagged.tag === SSKR_SHARE.value || tagged.tag === SSKR_SHARE_V1.value) {
|
|
233
|
+
const content = tagged.value as { buffer?: Uint8Array };
|
|
234
|
+
cbor = content as unknown as ReturnType<typeof toByteString>;
|
|
235
|
+
}
|
|
236
|
+
} catch {
|
|
237
|
+
// Not tagged CBOR, use as-is
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// The CBOR should be a byte string
|
|
242
|
+
const bytes = expectBytes(cbor);
|
|
243
|
+
untaggedCborShares.push(bytes);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return fromUntaggedCborShares(untaggedCborShares);
|
|
247
|
+
} catch {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function parseSskrSeed(input: string): Seed {
|
|
253
|
+
// Try envelope format first
|
|
254
|
+
const envelopeResult = parseEnvelopes(input);
|
|
255
|
+
if (envelopeResult !== null) {
|
|
256
|
+
return envelopeResult;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Try bytewords standard format
|
|
260
|
+
const btwResult = parseBytewords(input, BytewordsStyle.Standard);
|
|
261
|
+
if (btwResult !== null) {
|
|
262
|
+
return btwResult;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Try bytewords minimal format
|
|
266
|
+
const btwmResult = parseBytewords(input, BytewordsStyle.Minimal);
|
|
267
|
+
if (btwmResult !== null) {
|
|
268
|
+
return btwmResult;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Try bytewords uri format
|
|
272
|
+
const btwuResult = parseBytewords(input, BytewordsStyle.Uri);
|
|
273
|
+
if (btwuResult !== null) {
|
|
274
|
+
return btwuResult;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Try UR format (current tag)
|
|
278
|
+
const urResult = parseUr(input, SSKR_SHARE.value, false);
|
|
279
|
+
if (urResult !== null) {
|
|
280
|
+
return urResult;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Try legacy UR format (v1 tag, allow tagged cbor)
|
|
284
|
+
const urLegacyResult = parseUr(input, SSKR_SHARE_V1.value, true);
|
|
285
|
+
if (urLegacyResult !== null) {
|
|
286
|
+
return urLegacyResult;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
throw new Error("Insufficient or invalid SSKR shares.");
|
|
290
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* seedtool-cli
|
|
3
|
+
* A tool for generating and transforming cryptographic seeds.
|
|
4
|
+
* Ported from seedtool-cli-rust
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Re-export CLI types
|
|
8
|
+
export {
|
|
9
|
+
Cli,
|
|
10
|
+
InputFormatKey,
|
|
11
|
+
OutputFormatKey,
|
|
12
|
+
SSKRFormatKey,
|
|
13
|
+
type RngSource,
|
|
14
|
+
parseLowInt,
|
|
15
|
+
parseHighInt,
|
|
16
|
+
parseGroupThreshold,
|
|
17
|
+
parseDate,
|
|
18
|
+
parseGroupSpec,
|
|
19
|
+
} from "./cli.js";
|
|
20
|
+
|
|
21
|
+
// Re-export Seed type
|
|
22
|
+
export { Seed } from "./seed.js";
|
|
23
|
+
|
|
24
|
+
// Re-export random utilities
|
|
25
|
+
export {
|
|
26
|
+
DeterministicRandomNumberGenerator,
|
|
27
|
+
hkdfHmacSha256,
|
|
28
|
+
sha256DeterministicRandom,
|
|
29
|
+
deterministicRandom,
|
|
30
|
+
} from "./random.js";
|
|
31
|
+
|
|
32
|
+
// Re-export utility functions
|
|
33
|
+
export {
|
|
34
|
+
dataToHex,
|
|
35
|
+
hexToData,
|
|
36
|
+
dataToBase,
|
|
37
|
+
dataToAlphabet,
|
|
38
|
+
parseInts,
|
|
39
|
+
dataToInts,
|
|
40
|
+
digitsToData,
|
|
41
|
+
} from "./util.js";
|
|
42
|
+
|
|
43
|
+
// Re-export formats
|
|
44
|
+
export * from "./formats/index.js";
|
package/src/main.ts
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tool for generating and transforming cryptographic seeds.
|
|
3
|
+
* Ported from seedtool-cli-rust/src/main.rs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Command, Option } from "commander";
|
|
7
|
+
import { SecureRandomNumberGenerator } from "@bcts/rand";
|
|
8
|
+
import { SSKRGroupSpec } from "@bcts/components";
|
|
9
|
+
import { Cli, type InputFormatKey, type OutputFormatKey, type SSKRFormatKey } from "./cli.js";
|
|
10
|
+
import { selectInputFormat, selectOutputFormat } from "./formats/index.js";
|
|
11
|
+
import { DeterministicRandomNumberGenerator } from "./random.js";
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
|
|
14
|
+
const VERSION = "0.4.0";
|
|
15
|
+
|
|
16
|
+
function parseLowInt(value: string): number {
|
|
17
|
+
const num = parseInt(value, 10);
|
|
18
|
+
if (isNaN(num) || num < 0 || num > 254) {
|
|
19
|
+
throw new Error("LOW must be between 0 and 254");
|
|
20
|
+
}
|
|
21
|
+
return num;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseHighInt(value: string): number {
|
|
25
|
+
const num = parseInt(value, 10);
|
|
26
|
+
if (isNaN(num) || num < 1 || num > 255) {
|
|
27
|
+
throw new Error("HIGH must be between 1 and 255");
|
|
28
|
+
}
|
|
29
|
+
return num;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseGroupThreshold(value: string): number {
|
|
33
|
+
const num = parseInt(value, 10);
|
|
34
|
+
if (isNaN(num) || num < 1 || num > 16) {
|
|
35
|
+
throw new Error("THRESHOLD must be between 1 and 16");
|
|
36
|
+
}
|
|
37
|
+
return num;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseCliDate(value: string): Date {
|
|
41
|
+
if (value === "now") {
|
|
42
|
+
return new Date();
|
|
43
|
+
}
|
|
44
|
+
const date = new Date(value);
|
|
45
|
+
if (isNaN(date.getTime())) {
|
|
46
|
+
throw new Error(`Invalid date: ${value}. Use ISO-8601 format or 'now'.`);
|
|
47
|
+
}
|
|
48
|
+
return date;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseGroupSpec(value: string, previous: SSKRGroupSpec[]): SSKRGroupSpec[] {
|
|
52
|
+
const spec = SSKRGroupSpec.parse(value);
|
|
53
|
+
return [...previous, spec];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function main(): Promise<void> {
|
|
57
|
+
const program = new Command();
|
|
58
|
+
|
|
59
|
+
program
|
|
60
|
+
.name("seedtool")
|
|
61
|
+
.description(
|
|
62
|
+
"A tool for generating and transforming cryptographic seeds.\n\n" +
|
|
63
|
+
"by Wolf McNally and Christopher Allen\n\n" +
|
|
64
|
+
"Report bugs to ChristopherA@BlockchainCommons.com.\n" +
|
|
65
|
+
"© 2024 Blockchain Commons.",
|
|
66
|
+
)
|
|
67
|
+
.version(VERSION)
|
|
68
|
+
.argument(
|
|
69
|
+
"[INPUT]",
|
|
70
|
+
"The input to be transformed. If required and not present, it will be read from stdin.",
|
|
71
|
+
)
|
|
72
|
+
.option(
|
|
73
|
+
"-c, --count <COUNT>",
|
|
74
|
+
"The number of output units (hex bytes, base-10 digits, etc.)",
|
|
75
|
+
"16",
|
|
76
|
+
)
|
|
77
|
+
.addOption(
|
|
78
|
+
new Option(
|
|
79
|
+
"-i, --in <INPUT_TYPE>",
|
|
80
|
+
"The input format. If not specified, a new random seed is generated using a secure random number generator.",
|
|
81
|
+
)
|
|
82
|
+
.choices([
|
|
83
|
+
"random",
|
|
84
|
+
"hex",
|
|
85
|
+
"btw",
|
|
86
|
+
"btwm",
|
|
87
|
+
"btwu",
|
|
88
|
+
"bits",
|
|
89
|
+
"cards",
|
|
90
|
+
"dice",
|
|
91
|
+
"base6",
|
|
92
|
+
"base10",
|
|
93
|
+
"ints",
|
|
94
|
+
"bip39",
|
|
95
|
+
"sskr",
|
|
96
|
+
"envelope",
|
|
97
|
+
"seed",
|
|
98
|
+
"multipart",
|
|
99
|
+
])
|
|
100
|
+
.default("random"),
|
|
101
|
+
)
|
|
102
|
+
.addOption(
|
|
103
|
+
new Option("-o, --out <OUTPUT_TYPE>", "The output format.")
|
|
104
|
+
.choices([
|
|
105
|
+
"hex",
|
|
106
|
+
"btw",
|
|
107
|
+
"btwm",
|
|
108
|
+
"btwu",
|
|
109
|
+
"bits",
|
|
110
|
+
"cards",
|
|
111
|
+
"dice",
|
|
112
|
+
"base6",
|
|
113
|
+
"base10",
|
|
114
|
+
"ints",
|
|
115
|
+
"bip39",
|
|
116
|
+
"sskr",
|
|
117
|
+
"envelope",
|
|
118
|
+
"seed",
|
|
119
|
+
"multipart",
|
|
120
|
+
])
|
|
121
|
+
.default("hex"),
|
|
122
|
+
)
|
|
123
|
+
.option("--low <LOW>", "The lowest int returned (0-254)", parseLowInt, 0)
|
|
124
|
+
.option("--high <HIGH>", "The highest int returned (1-255), low < high", parseHighInt, 9)
|
|
125
|
+
.option("--name <NAME>", "The name of the seed.")
|
|
126
|
+
.option("--note <NOTE>", "The note associated with the seed.")
|
|
127
|
+
.option("--date <DATE>", "The seed's creation date, in ISO-8601 format. May also be `now`.")
|
|
128
|
+
.option(
|
|
129
|
+
"--max-fragment-len <MAX_FRAG_LEN>",
|
|
130
|
+
"For `multipart` output, the UR will be segmented into parts with fragments no larger than MAX_FRAG_LEN",
|
|
131
|
+
"500",
|
|
132
|
+
)
|
|
133
|
+
.option(
|
|
134
|
+
"--additional-parts <NUM_PARTS>",
|
|
135
|
+
"For `multipart` output, the number of additional parts above the minimum to generate using fountain encoding.",
|
|
136
|
+
"0",
|
|
137
|
+
)
|
|
138
|
+
.option(
|
|
139
|
+
"-g, --groups <M-of-N>",
|
|
140
|
+
"Group specifications. May appear more than once. M must be < N",
|
|
141
|
+
parseGroupSpec,
|
|
142
|
+
[],
|
|
143
|
+
)
|
|
144
|
+
.option(
|
|
145
|
+
"-t, --group-threshold <THRESHOLD>",
|
|
146
|
+
"The number of groups that must meet their threshold. Must be <= the number of group specifications.",
|
|
147
|
+
parseGroupThreshold,
|
|
148
|
+
1,
|
|
149
|
+
)
|
|
150
|
+
.addOption(
|
|
151
|
+
new Option("-s, --sskr-format <SSKR_FORMAT>", "SSKR output format.")
|
|
152
|
+
.choices(["envelope", "btw", "btwm", "btwu", "ur"])
|
|
153
|
+
.default("envelope"),
|
|
154
|
+
)
|
|
155
|
+
.option(
|
|
156
|
+
"-d, --deterministic <SEED_STRING>",
|
|
157
|
+
"Use a deterministic random number generator with the given seed string. Output generated from this seed will be the same every time, so generated seeds are only as secure as the seed string.",
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
program.parse();
|
|
161
|
+
|
|
162
|
+
const options = program.opts();
|
|
163
|
+
const args = program.args;
|
|
164
|
+
|
|
165
|
+
// Create the CLI state
|
|
166
|
+
const cli = new Cli();
|
|
167
|
+
|
|
168
|
+
// Set input from argument or stdin
|
|
169
|
+
if (args.length > 0) {
|
|
170
|
+
cli.input = args[0];
|
|
171
|
+
} else if (!process.stdin.isTTY) {
|
|
172
|
+
// Read from stdin if it's piped
|
|
173
|
+
cli.input = fs.readFileSync(0, "utf-8").trim();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Set options
|
|
177
|
+
cli.count = parseInt(options.count, 10);
|
|
178
|
+
cli.in = options.in as InputFormatKey;
|
|
179
|
+
cli.out = options.out as OutputFormatKey;
|
|
180
|
+
cli.low = options.low;
|
|
181
|
+
cli.high = options.high;
|
|
182
|
+
cli.name = options.name;
|
|
183
|
+
cli.note = options.note;
|
|
184
|
+
if (options.date) {
|
|
185
|
+
cli.date = parseCliDate(options.date);
|
|
186
|
+
}
|
|
187
|
+
cli.maxFragmentLen = parseInt(options.maxFragmentLen, 10);
|
|
188
|
+
cli.additionalParts = parseInt(options.additionalParts, 10);
|
|
189
|
+
cli.groups = options.groups;
|
|
190
|
+
cli.groupThreshold = options.groupThreshold;
|
|
191
|
+
cli.sskrFormat = options.sskrFormat as SSKRFormatKey;
|
|
192
|
+
|
|
193
|
+
// Set up RNG
|
|
194
|
+
if (options.deterministic) {
|
|
195
|
+
cli.rng = {
|
|
196
|
+
type: "deterministic",
|
|
197
|
+
rng: DeterministicRandomNumberGenerator.newWithSeed(options.deterministic),
|
|
198
|
+
};
|
|
199
|
+
} else {
|
|
200
|
+
cli.rng = {
|
|
201
|
+
type: "secure",
|
|
202
|
+
rng: new SecureRandomNumberGenerator(),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Get formats
|
|
207
|
+
const inputFormat = selectInputFormat(cli.in);
|
|
208
|
+
const outputFormat = selectOutputFormat(cli.out);
|
|
209
|
+
|
|
210
|
+
// Validate round-trippability
|
|
211
|
+
if (!outputFormat.roundTrippable() && inputFormat.name() !== "random") {
|
|
212
|
+
console.error(`Input for output form "${outputFormat.name()}" must be random.`);
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Process input
|
|
217
|
+
const processedCli = inputFormat.processInput(cli);
|
|
218
|
+
|
|
219
|
+
// Process output
|
|
220
|
+
const output = outputFormat.processOutput(processedCli);
|
|
221
|
+
console.log(output);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
main().catch((error) => {
|
|
225
|
+
console.error(error.message);
|
|
226
|
+
process.exit(1);
|
|
227
|
+
});
|