Skip to content

Commit ea382c0

Browse files
Merge pull request #185 from DIG-Network/release/v0.0.1-alpha.196
Release/v0.0.1 alpha.196
2 parents 614bd46 + 701f3db commit ea382c0

File tree

5 files changed

+219
-4
lines changed

5 files changed

+219
-4
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
44

5+
### [0.0.1-alpha.196](https://github.com/DIG-Network/dig-chia-sdk/compare/v0.0.1-alpha.195...v0.0.1-alpha.196) (2024-11-17)
6+
7+
8+
### Features
9+
10+
* add subdomain encoder ([f9f2da4](https://github.com/DIG-Network/dig-chia-sdk/commit/f9f2da499dff1c2b158945be3ca238cad9de0f3a))
11+
512
### [0.0.1-alpha.195](https://github.com/DIG-Network/dig-chia-sdk/compare/v0.0.1-alpha.194...v0.0.1-alpha.195) (2024-11-16)
613

714

package-lock.json

Lines changed: 8 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@dignetwork/dig-sdk",
3-
"version": "0.0.1-alpha.195",
3+
"version": "0.0.1-alpha.196",
44
"description": "",
55
"type": "commonjs",
66
"main": "./dist/index.js",
@@ -30,6 +30,7 @@
3030
"archiver": "^7.0.1",
3131
"async-mutex": "^0.5.0",
3232
"axios": "^1.7.7",
33+
"base-x": "^5.0.0",
3334
"bip39": "^3.1.0",
3435
"bottleneck": "^2.19.5",
3536
"chia-bls": "^1.0.2",

src/utils/Subdomain.ts

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
// SubDomain.ts
2+
3+
import baseX from "base-x";
4+
import crypto from "crypto";
5+
6+
/**
7+
* SubDomain Class
8+
*
9+
* Encapsulates the logic for encoding and decoding a combination of
10+
* chain and storeId into a DNS-friendly identifier using Base62 encoding and HMAC.
11+
*/
12+
class SubDomain {
13+
// Define the Base62 character set
14+
private static readonly BASE62_CHARSET =
15+
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
16+
17+
// Initialize the Base62 encoder/decoder
18+
private static base62 = baseX(SubDomain.BASE62_CHARSET);
19+
20+
// Define expected byte length for storeId
21+
private static readonly DEFAULT_STORE_ID_LENGTH = 32; // bytes
22+
private static readonly HMAC_LENGTH = 32; // bytes for HMAC-SHA256
23+
24+
// Hardcoded compression key
25+
private static readonly COMPRESSION_KEY =
26+
"7a4e8d2f6b1c9a3f5d8e2c4b7a1f9d3e6b8c5a2f4d7e9b1c8a3f5d2e6b9c4a7";
27+
28+
// Properties
29+
public readonly chain: string;
30+
public readonly storeId: string;
31+
public readonly encodedId: string;
32+
33+
/**
34+
* Constructor for SubDomain
35+
*
36+
* @param chain - The chain name (e.g., "CHAIN1234").
37+
* @param storeId - The store ID as a 64-character hexadecimal string.
38+
* @throws Will throw an error if inputs are invalid or encoding exceeds DNS limits.
39+
*/
40+
constructor(chain: string, storeId: string) {
41+
this.chain = chain;
42+
this.storeId = storeId;
43+
this.encodedId = this.encode();
44+
}
45+
46+
/**
47+
* Encodes the provided chain and storeId into a DNS-friendly identifier with HMAC.
48+
*
49+
* @returns The Base62-encoded identifier with appended HMAC.
50+
* @throws Will throw an error if encoding fails.
51+
*/
52+
private encode(): string {
53+
// Validate inputs
54+
if (!this.chain || typeof this.chain !== "string") {
55+
throw new Error("Invalid chain: Chain must be a non-empty string.");
56+
}
57+
58+
if (
59+
!this.storeId ||
60+
typeof this.storeId !== "string" ||
61+
!/^[0-9a-fA-F]{64}$/.test(this.storeId)
62+
) {
63+
throw new Error(
64+
"Invalid storeId: StoreId must be a 64-character hexadecimal string."
65+
);
66+
}
67+
68+
// Ensure the chain length is within 1-255 characters to fit in one byte
69+
const chainLength = this.chain.length;
70+
if (chainLength < 1 || chainLength > 255) {
71+
throw new Error(
72+
"Invalid chain: Length must be between 1 and 255 characters."
73+
);
74+
}
75+
76+
// Convert chain length to a single byte Buffer
77+
const chainLengthBuffer = Buffer.from([chainLength]);
78+
79+
// Convert chain to a Buffer (UTF-8)
80+
const chainBuffer = Buffer.from(this.chain, "utf8");
81+
82+
// Convert storeId from hex string to Buffer
83+
const storeIdBuffer = Buffer.from(this.storeId, "hex");
84+
85+
// Validate storeId byte length
86+
if (storeIdBuffer.length !== SubDomain.DEFAULT_STORE_ID_LENGTH) {
87+
throw new Error(
88+
`Invalid storeId length: Expected ${SubDomain.DEFAULT_STORE_ID_LENGTH} bytes, got ${storeIdBuffer.length} bytes.`
89+
);
90+
}
91+
92+
// Concatenate chain_length, chain, and storeId buffers
93+
const dataBuffer = Buffer.concat([
94+
chainLengthBuffer,
95+
chainBuffer,
96+
storeIdBuffer,
97+
]);
98+
99+
// Create HMAC using SHA256
100+
const hmac = crypto.createHmac("sha256", SubDomain.COMPRESSION_KEY);
101+
hmac.update(dataBuffer);
102+
const hmacDigest = hmac.digest(); // 32 bytes
103+
104+
// Concatenate dataBuffer and hmacDigest
105+
const finalBuffer = Buffer.concat([dataBuffer, hmacDigest]);
106+
107+
// Encode the final buffer using Base62
108+
const encodedId = SubDomain.base62.encode(finalBuffer);
109+
110+
// Ensure DNS label length does not exceed 63 characters
111+
if (encodedId.length > 63) {
112+
throw new Error(
113+
`Encoded identifier length (${encodedId.length}) exceeds DNS label limit of 63 characters.`
114+
);
115+
}
116+
117+
return encodedId;
118+
}
119+
120+
/**
121+
* Decodes the provided identifier back into the original chain and storeId after verifying HMAC.
122+
*
123+
* @param encodedId - The Base62-encoded identifier with appended HMAC.
124+
* @returns An object containing the original chain and storeId.
125+
* @throws Will throw an error if decoding fails, HMAC verification fails, or data lengths mismatch.
126+
*/
127+
public static decode(encodedId: string): { chain: string; storeId: string } {
128+
// Validate input
129+
if (!encodedId || typeof encodedId !== "string") {
130+
throw new Error(
131+
"Invalid encodedId: encodedId must be a non-empty string."
132+
);
133+
}
134+
135+
// Decode the Base62 string back to a Buffer
136+
const decodedBuffer = SubDomain.base62.decode(encodedId);
137+
138+
if (!decodedBuffer) {
139+
throw new Error("Failed to decode Base62 string.");
140+
}
141+
142+
// Ensure there's at least 1 byte for chain_length and 64 bytes for storeId and HMAC
143+
if (
144+
decodedBuffer.length <
145+
1 + SubDomain.DEFAULT_STORE_ID_LENGTH + SubDomain.HMAC_LENGTH
146+
) {
147+
throw new Error("Decoded data is too short to contain required fields.");
148+
}
149+
150+
// Extract chain_length (1 byte)
151+
const chain_length = Buffer.from(decodedBuffer).readUInt8(0);
152+
153+
// Define the expected total length
154+
const expected_length =
155+
1 +
156+
chain_length +
157+
SubDomain.DEFAULT_STORE_ID_LENGTH +
158+
SubDomain.HMAC_LENGTH;
159+
160+
if (decodedBuffer.length !== expected_length) {
161+
throw new Error(
162+
`Decoded data length mismatch: expected ${expected_length} bytes, got ${decodedBuffer.length} bytes.`
163+
);
164+
}
165+
166+
// Extract chain, storeId, and received HMAC from the buffer
167+
const chain = Buffer.from(
168+
decodedBuffer.slice(1, 1 + chain_length)
169+
).toString("utf8");
170+
const storeIdBuffer = decodedBuffer.slice(
171+
1 + chain_length,
172+
1 + chain_length + SubDomain.DEFAULT_STORE_ID_LENGTH
173+
);
174+
const receivedHmac = decodedBuffer.slice(
175+
1 + chain_length + SubDomain.DEFAULT_STORE_ID_LENGTH,
176+
expected_length
177+
);
178+
179+
// Recompute HMAC over [chain_length][chain][storeId]
180+
const dataBuffer = decodedBuffer.slice(
181+
0,
182+
1 + chain_length + SubDomain.DEFAULT_STORE_ID_LENGTH
183+
);
184+
const hmac = crypto.createHmac("sha256", SubDomain.COMPRESSION_KEY);
185+
hmac.update(dataBuffer);
186+
const expectedHmac = hmac.digest(); // 32 bytes
187+
188+
// Compare HMACs securely
189+
if (!crypto.timingSafeEqual(receivedHmac, expectedHmac)) {
190+
throw new Error("HMAC verification failed: Invalid identifier.");
191+
}
192+
193+
// Convert storeId buffer to hex string
194+
const storeId = Buffer.from(storeIdBuffer).toString("hex");
195+
196+
return { chain, storeId };
197+
}
198+
}
199+
200+
export { SubDomain };

src/utils/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ export * from './Environment';
1414
export * from './promiseUtils';
1515
export * from './PeerRanker';
1616
export * from './DigCache';
17-
export * from './Udi';
17+
export * from './Udi';
18+
export * from './Subdomain';

0 commit comments

Comments
 (0)