i2pseeds 2026.7.13 → 2026.7.27

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/dist/cli.js CHANGED
@@ -1,20 +1,178 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ SIG_TYPES,
4
+ createSu3,
3
5
  extractNetDb,
4
- refreshSeeds
5
- } from "./shared/chunk-2mehkkf3.js";
6
+ parseSu3Header,
7
+ refreshSeeds,
8
+ verifySu3Signature
9
+ } from "./shared/chunk-ckgwjd1a.js";
6
10
 
7
11
  // src/cli.ts
8
12
  import fs from "node:fs/promises";
9
13
  import os from "node:os";
10
14
  import path from "node:path";
11
15
  import { fileURLToPath } from "node:url";
16
+ import AdmZip from "adm-zip";
12
17
  var __dirname2 = path.dirname(fileURLToPath(import.meta.url));
18
+ function printHelp() {
19
+ console.log(`
20
+ Usage:
21
+ su3 netdb [--refresh] Fetch seeds and extract netdb router entries
22
+ su3 info <file.su3> Print details of an SU3 file header
23
+ su3 verify <file.su3> <cert.crt> Verify signature of an SU3 file
24
+ su3 extract <file.su3> <outputFile> Extract the raw payload from an SU3 file
25
+ su3 pack <inputDirOrFile> <outFile.su3> [opts] Package and sign content into an SU3 file
26
+
27
+ Options for pack:
28
+ --signerId <id> Signer identity (required, e.g., sahil@mail.i2p)
29
+ --privateKey <file> Path to the private key PEM file (required)
30
+ --version <version> Version string (default: current Unix epoch seconds)
31
+ --contentType <type> Content type ID (default: 3 for reseed)
32
+ --fileType <type> File type ID (default: 0 for zip, auto-detected otherwise)
33
+ --sigType <type> Signature type ID (default: 6 for RSA-SHA512-4096)
34
+ `);
35
+ }
13
36
  async function main() {
14
37
  const args = process.argv.slice(2);
15
38
  const command = args[0];
39
+ if (!command || args.includes("--help") || args.includes("-h")) {
40
+ printHelp();
41
+ process.exit(0);
42
+ }
43
+ if (command === "info") {
44
+ const filePath = args[1];
45
+ if (!filePath) {
46
+ console.error("Error: file path is required.");
47
+ printHelp();
48
+ process.exit(1);
49
+ }
50
+ const buf = await fs.readFile(filePath);
51
+ const header = parseSu3Header(buf);
52
+ if (!header) {
53
+ console.error("Invalid SU3 file: Magic header not found");
54
+ process.exit(1);
55
+ }
56
+ const sigDetails = SIG_TYPES[header.sigType];
57
+ console.log("SU3 Header Details:");
58
+ console.log(` Signer ID: ${header.signerId}`);
59
+ console.log(` Version: ${header.version}`);
60
+ console.log(` Signature Type: ${header.sigType} (${sigDetails?.name ?? "Unknown"})`);
61
+ console.log(` Signature Length: ${header.sigLength} bytes`);
62
+ console.log(` Content Length: ${header.contentLength} bytes`);
63
+ console.log(` Content Type: ${header.contentType}`);
64
+ console.log(` File Type: ${header.fileType}`);
65
+ process.exit(0);
66
+ }
67
+ if (command === "verify") {
68
+ const filePath = args[1];
69
+ const certPath = args[2];
70
+ if (!filePath || !certPath) {
71
+ console.error("Error: file path and cert path are required.");
72
+ printHelp();
73
+ process.exit(1);
74
+ }
75
+ const buf = await fs.readFile(filePath);
76
+ const certPem = await fs.readFile(certPath, "utf8");
77
+ const isValid = verifySu3Signature(buf, certPem);
78
+ if (isValid) {
79
+ console.log("Signature is VALID");
80
+ process.exit(0);
81
+ } else {
82
+ console.log("Signature is INVALID");
83
+ process.exit(1);
84
+ }
85
+ }
86
+ if (command === "extract") {
87
+ const filePath = args[1];
88
+ const outPath = args[2];
89
+ if (!filePath || !outPath) {
90
+ console.error("Error: input file path and output file path are required.");
91
+ printHelp();
92
+ process.exit(1);
93
+ }
94
+ const buf = await fs.readFile(filePath);
95
+ const header = parseSu3Header(buf);
96
+ if (!header) {
97
+ console.error("Invalid SU3 file: Magic header not found");
98
+ process.exit(1);
99
+ }
100
+ const payloadStart = 40 + header.versionLength + header.signerIdLength;
101
+ const payload = buf.subarray(payloadStart, payloadStart + header.contentLength);
102
+ await fs.writeFile(outPath, payload);
103
+ console.log(`Extracted raw payload (${payload.length} bytes) to ${outPath}`);
104
+ process.exit(0);
105
+ }
106
+ if (command === "pack") {
107
+ const inputPath = args[1];
108
+ const outputPath = args[2];
109
+ if (!inputPath || !outputPath) {
110
+ console.error("Error: input path and output path are required.");
111
+ printHelp();
112
+ process.exit(1);
113
+ }
114
+ const getOpt = (flag) => {
115
+ const idx = args.indexOf(flag);
116
+ if (idx !== -1 && idx + 1 < args.length) {
117
+ return args[idx + 1];
118
+ }
119
+ return;
120
+ };
121
+ const signerId = getOpt("--signerId");
122
+ const privateKeyPath = getOpt("--privateKey");
123
+ if (!signerId) {
124
+ console.error("Error: --signerId is required.");
125
+ process.exit(1);
126
+ }
127
+ if (!privateKeyPath) {
128
+ console.error("Error: --privateKey path is required.");
129
+ process.exit(1);
130
+ }
131
+ const version = getOpt("--version") ?? Math.floor(Date.now() / 1000).toString();
132
+ const contentType = parseInt(getOpt("--contentType") ?? "3", 10);
133
+ const fileTypeOpt = getOpt("--fileType");
134
+ const sigType = parseInt(getOpt("--sigType") ?? "6", 10);
135
+ let content;
136
+ let fileType = fileTypeOpt !== undefined ? parseInt(fileTypeOpt, 10) : 0;
137
+ const stat = await fs.stat(inputPath);
138
+ if (stat.isDirectory()) {
139
+ console.log(`Zipping directory ${inputPath}...`);
140
+ const zip = new AdmZip;
141
+ zip.addLocalFolder(inputPath);
142
+ content = zip.toBuffer();
143
+ fileType = 0;
144
+ } else {
145
+ content = await fs.readFile(inputPath);
146
+ if (fileTypeOpt === undefined) {
147
+ if (inputPath.endsWith(".zip")) {
148
+ fileType = 0;
149
+ } else if (inputPath.endsWith(".xml")) {
150
+ fileType = 1;
151
+ } else if (inputPath.endsWith(".xml.gz")) {
152
+ fileType = 3;
153
+ } else {
154
+ fileType = 0;
155
+ }
156
+ }
157
+ }
158
+ const privateKeyPem = await fs.readFile(privateKeyPath, "utf8");
159
+ console.log(`Packaging & signing SU3 file (signer: ${signerId}, sigType: ${sigType})...`);
160
+ const su3Buf = createSu3({
161
+ content,
162
+ version,
163
+ signerId,
164
+ contentType,
165
+ fileType,
166
+ sigType,
167
+ privateKeyPem
168
+ });
169
+ await fs.writeFile(outputPath, su3Buf);
170
+ console.log(`Successfully created signed SU3 file: ${outputPath}`);
171
+ process.exit(0);
172
+ }
16
173
  if (command !== "netdb") {
17
- console.error("Usage: su3 netdb [--refresh]");
174
+ console.error(`Unknown command: ${command}`);
175
+ printHelp();
18
176
  process.exit(1);
19
177
  }
20
178
  const doRefresh = args.includes("--refresh");
@@ -45,6 +45,6 @@
45
45
  },
46
46
  {
47
47
  "url": "https://reseed.onion.im/",
48
- "crt_file": "lazygravy_at_mail.i2p"
48
+ "crt_file": "lazygravy_at_mail.i2p.crt"
49
49
  }
50
50
  ]
package/dist/index.d.ts CHANGED
@@ -2,6 +2,17 @@
2
2
  * The magic bytes that mark the start of a ZIP file inside an SU3 container.
3
3
  */
4
4
  declare const ZIP_MAGIC: Buffer;
5
+ interface SigTypeDetails {
6
+ name: string;
7
+ hash: string | null;
8
+ dsaEncoding?: "ieee-p1363";
9
+ sigLength: number;
10
+ }
11
+ /**
12
+ * Standard I2P SU3 signature types mapping.
13
+ * Spec: https://geti2p.net/en/docs/spec/updates#signature-details
14
+ */
15
+ declare const SIG_TYPES: Record<number, SigTypeDetails>;
5
16
  interface Su3Header {
6
17
  sigType: number;
7
18
  sigLength: number;
@@ -27,6 +38,19 @@ declare function signerIdToCertFile(signerId: string): string;
27
38
  * Returns true if valid, false otherwise.
28
39
  */
29
40
  declare function verifySu3Signature(su3Buffer: Buffer, certPem: string): boolean;
41
+ interface CreateSu3Params {
42
+ content: Buffer;
43
+ version: string;
44
+ signerId: string;
45
+ contentType: number;
46
+ fileType: number;
47
+ sigType: number;
48
+ privateKeyPem: string;
49
+ }
50
+ /**
51
+ * Packages and signs content inside a valid SU3 container.
52
+ */
53
+ declare function createSu3(params: CreateSu3Params): Buffer;
30
54
  /**
31
55
  * Strips the SU3 header from a raw buffer and returns just the ZIP payload.
32
56
  * Returns `null` if no ZIP header is found.
@@ -54,4 +78,4 @@ interface ReseedServer {
54
78
  * Uses the built-in reseed.json list or a user-supplied one.
55
79
  */
56
80
  declare function refreshSeeds(servers: ReseedServer[], outputDir: string, timeoutMs?: number): Promise<string[]>;
57
- export { verifySu3Signature, signerIdToCertFile, refreshSeeds, parseSu3Header, extractZipFromSu3, extractNetDb, ZIP_MAGIC, Su3Header, ReseedServer, ExtractResult };
81
+ export { verifySu3Signature, signerIdToCertFile, refreshSeeds, parseSu3Header, extractZipFromSu3, extractNetDb, createSu3, ZIP_MAGIC, Su3Header, SigTypeDetails, SIG_TYPES, ReseedServer, ExtractResult, CreateSu3Params };
package/dist/index.js CHANGED
@@ -1,12 +1,14 @@
1
1
  import {
2
+ SIG_TYPES,
2
3
  ZIP_MAGIC,
4
+ createSu3,
3
5
  extractNetDb,
4
6
  extractZipFromSu3,
5
7
  parseSu3Header,
6
8
  refreshSeeds,
7
9
  signerIdToCertFile,
8
10
  verifySu3Signature
9
- } from "./shared/chunk-2mehkkf3.js";
11
+ } from "./shared/chunk-ckgwjd1a.js";
10
12
  export {
11
13
  verifySu3Signature,
12
14
  signerIdToCertFile,
@@ -14,5 +16,7 @@ export {
14
16
  parseSu3Header,
15
17
  extractZipFromSu3,
16
18
  extractNetDb,
17
- ZIP_MAGIC
19
+ createSu3,
20
+ ZIP_MAGIC,
21
+ SIG_TYPES
18
22
  };
@@ -4,12 +4,35 @@ import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import AdmZip from "adm-zip";
6
6
  var ZIP_MAGIC = Buffer.from([80, 75, 3, 4]);
7
- var SIG_ALGORITHMS = {
8
- 3: "SHA256",
9
- 4: "SHA256",
10
- 5: "SHA384",
11
- 6: "SHA512",
12
- 8: "SHA512"
7
+ var SIG_TYPES = {
8
+ 0: {
9
+ name: "DSA-SHA1",
10
+ hash: "SHA1",
11
+ dsaEncoding: "ieee-p1363",
12
+ sigLength: 40
13
+ },
14
+ 1: {
15
+ name: "ECDSA-SHA256-P256",
16
+ hash: "SHA256",
17
+ dsaEncoding: "ieee-p1363",
18
+ sigLength: 64
19
+ },
20
+ 2: {
21
+ name: "ECDSA-SHA384-P384",
22
+ hash: "SHA384",
23
+ dsaEncoding: "ieee-p1363",
24
+ sigLength: 96
25
+ },
26
+ 3: {
27
+ name: "ECDSA-SHA512-P521",
28
+ hash: "SHA512",
29
+ dsaEncoding: "ieee-p1363",
30
+ sigLength: 132
31
+ },
32
+ 4: { name: "RSA-SHA256-2048", hash: "SHA256", sigLength: 256 },
33
+ 5: { name: "RSA-SHA384-3072", hash: "SHA384", sigLength: 384 },
34
+ 6: { name: "RSA-SHA512-4096", hash: "SHA512", sigLength: 512 },
35
+ 8: { name: "EdDSA-SHA512-Ed25519ph", hash: null, sigLength: 64 }
13
36
  };
14
37
  function parseSu3Header(buf) {
15
38
  if (buf.length < 40)
@@ -22,8 +45,8 @@ function parseSu3Header(buf) {
22
45
  const versionLength = buf[13] ?? 0;
23
46
  const signerIdLength = buf[15] ?? 0;
24
47
  const contentLength = buf.readUInt32BE(16) * 4294967296 + buf.readUInt32BE(20);
25
- const fileType = buf[25] ?? 0;
26
- const contentType = buf[27] ?? 0;
48
+ const contentType = buf[25] ?? 0;
49
+ const fileType = buf[27] ?? 0;
27
50
  const headerEnd = 40;
28
51
  const version = buf.subarray(headerEnd, headerEnd + versionLength).toString("ascii").replace(/\0/g, "");
29
52
  const signerId = buf.subarray(headerEnd + versionLength, headerEnd + versionLength + signerIdLength).toString("ascii");
@@ -46,8 +69,8 @@ function verifySu3Signature(su3Buffer, certPem) {
46
69
  const header = parseSu3Header(su3Buffer);
47
70
  if (!header)
48
71
  return false;
49
- const algo = SIG_ALGORITHMS[header.sigType];
50
- if (!algo) {
72
+ const sigTypeDetails = SIG_TYPES[header.sigType];
73
+ if (!sigTypeDetails) {
51
74
  console.error(`Unknown SU3 signature type: ${header.sigType}`);
52
75
  return false;
53
76
  }
@@ -56,14 +79,75 @@ function verifySu3Signature(su3Buffer, certPem) {
56
79
  const signedData = su3Buffer.subarray(0, signedDataEnd);
57
80
  const signature = su3Buffer.subarray(signedDataEnd, signedDataEnd + header.sigLength);
58
81
  try {
59
- const verifier = crypto.createVerify(algo);
60
- verifier.update(signedData);
61
- return verifier.verify(certPem, signature);
82
+ if (header.sigType === 8) {
83
+ return crypto.verify(null, signedData, certPem, signature);
84
+ }
85
+ const options = {
86
+ key: certPem
87
+ };
88
+ if (sigTypeDetails.dsaEncoding) {
89
+ options.dsaEncoding = sigTypeDetails.dsaEncoding;
90
+ }
91
+ return crypto.verify(sigTypeDetails.hash, signedData, options, signature);
62
92
  } catch (err) {
63
93
  console.error("Signature verification error:", err);
64
94
  return false;
65
95
  }
66
96
  }
97
+ function createSu3(params) {
98
+ const sigTypeDetails = SIG_TYPES[params.sigType];
99
+ if (!sigTypeDetails) {
100
+ throw new Error(`Unsupported signature type: ${params.sigType}`);
101
+ }
102
+ const versionBuf = Buffer.alloc(16);
103
+ versionBuf.write(params.version, "ascii");
104
+ const signerBuf = Buffer.from(params.signerId, "utf8");
105
+ const header = Buffer.alloc(40);
106
+ header.write("I2Psu3", 0, "ascii");
107
+ header[6] = 0;
108
+ header[7] = 0;
109
+ header.writeUInt16BE(params.sigType, 8);
110
+ header.writeUInt16BE(sigTypeDetails.sigLength, 10);
111
+ header[12] = 0;
112
+ header[13] = versionBuf.length;
113
+ header[14] = 0;
114
+ header[15] = signerBuf.length;
115
+ const contentLength = params.content.length;
116
+ header.writeUInt32BE(0, 16);
117
+ header.writeUInt32BE(contentLength, 20);
118
+ header[24] = 0;
119
+ header[25] = params.contentType;
120
+ header[26] = 0;
121
+ header[27] = params.fileType;
122
+ const signedData = Buffer.concat([
123
+ header,
124
+ versionBuf,
125
+ signerBuf,
126
+ params.content
127
+ ]);
128
+ let signature;
129
+ if (params.sigType === 8) {
130
+ signature = crypto.sign(null, signedData, params.privateKeyPem);
131
+ } else {
132
+ const options = {
133
+ key: params.privateKeyPem
134
+ };
135
+ if (sigTypeDetails.dsaEncoding) {
136
+ options.dsaEncoding = sigTypeDetails.dsaEncoding;
137
+ }
138
+ signature = crypto.sign(sigTypeDetails.hash, signedData, options);
139
+ }
140
+ if (signature.length !== sigTypeDetails.sigLength) {
141
+ if (signature.length < sigTypeDetails.sigLength) {
142
+ const padded = Buffer.alloc(sigTypeDetails.sigLength);
143
+ signature.copy(padded, sigTypeDetails.sigLength - signature.length);
144
+ signature = padded;
145
+ } else {
146
+ signature = signature.subarray(0, sigTypeDetails.sigLength);
147
+ }
148
+ }
149
+ return Buffer.concat([signedData, signature]);
150
+ }
67
151
  function extractZipFromSu3(su3Buffer) {
68
152
  const idx = su3Buffer.indexOf(ZIP_MAGIC);
69
153
  if (idx === -1)
@@ -171,4 +255,4 @@ async function refreshSeeds(servers, outputDir, timeoutMs = 1e4) {
171
255
  return saved;
172
256
  }
173
257
 
174
- export { ZIP_MAGIC, parseSu3Header, signerIdToCertFile, verifySu3Signature, extractZipFromSu3, extractNetDb, refreshSeeds };
258
+ export { ZIP_MAGIC, SIG_TYPES, parseSu3Header, signerIdToCertFile, verifySu3Signature, createSu3, extractZipFromSu3, extractNetDb, refreshSeeds };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i2pseeds",
3
- "version": "2026.07.13",
3
+ "version": "2026.07.27",
4
4
  "description": "TypeScript library and CLI for I2P network reseeding — parse SU3 files, verify signatures, and extract router info into netDb",
5
5
  "author": "labofsahil",
6
6
  "keywords": [