@qidcloud/sdk 1.1.0
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/README.md +310 -0
- package/dist/QidCloud.d.ts +31 -0
- package/dist/QidCloud.js +142 -0
- package/dist/crypto/pqc.d.ts +12 -0
- package/dist/crypto/pqc.js +91 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/storage/index.d.ts +18 -0
- package/dist/storage/index.js +58 -0
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +46 -0
- package/package.json +31 -0
- package/src/QidCloud.ts +171 -0
- package/src/crypto/pqc.ts +79 -0
- package/src/index.ts +7 -0
- package/src/storage/index.ts +49 -0
- package/src/utils.ts +48 -0
- package/tsconfig.json +20 -0
- package/verify_sdk.ts +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
# @pqcloud/sdk
|
|
2
|
+
|
|
3
|
+
Official JavaScript/TypeScript SDK for Q-Gate Authentication - Quantum-resistant, passwordless authentication for modern applications.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @pqcloud/sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { QGateClient } from '@pqcloud/sdk';
|
|
15
|
+
|
|
16
|
+
// Initialize with your API key
|
|
17
|
+
const qgate = new QGateClient({
|
|
18
|
+
apiKey: 'your-api-key-here'
|
|
19
|
+
// baseUrl is optional, defaults to https://qgate.onrender.com
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Register a new user
|
|
23
|
+
const user = await qgate.register('alice');
|
|
24
|
+
|
|
25
|
+
// Login
|
|
26
|
+
const token = await qgate.login();
|
|
27
|
+
|
|
28
|
+
// Get user profile
|
|
29
|
+
const profile = await qgate.getProfile();
|
|
30
|
+
|
|
31
|
+
// Logout
|
|
32
|
+
await qgate.logout();
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
✅ **Quantum-Resistant** - Uses CRYSTALS-Dilithium and Kyber
|
|
38
|
+
✅ **Passwordless** - No passwords to remember or manage
|
|
39
|
+
✅ **Zero-Knowledge** - Keys never leave the device
|
|
40
|
+
✅ **TypeScript Support** - Full type definitions included
|
|
41
|
+
✅ **Browser & Node.js** - Works everywhere
|
|
42
|
+
|
|
43
|
+
## API Reference
|
|
44
|
+
|
|
45
|
+
### `new QGateClient(config)`
|
|
46
|
+
|
|
47
|
+
Initialize the Q-Gate client.
|
|
48
|
+
|
|
49
|
+
**Parameters:**
|
|
50
|
+
- `config.apiKey` (required): Your Q-Gate API key
|
|
51
|
+
- `config.baseUrl` (optional): Custom backend URL, defaults to `https://qgate.onrender.com`
|
|
52
|
+
- `config.storage` (optional): Custom storage implementation (defaults to localStorage in browser, memory in Node.js)
|
|
53
|
+
|
|
54
|
+
**Example:**
|
|
55
|
+
```typescript
|
|
56
|
+
const qgate = new QGateClient({
|
|
57
|
+
apiKey: 'qg-abc123...',
|
|
58
|
+
baseUrl: 'https://custom-backend.com' // optional
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
### `qgate.register(username, tenantId?)`
|
|
65
|
+
|
|
66
|
+
Register a new user identity.
|
|
67
|
+
|
|
68
|
+
**Parameters:**
|
|
69
|
+
- `username` (required): Unique username
|
|
70
|
+
- `tenantId` (optional): Custom tenant ID, defaults to 'default'
|
|
71
|
+
|
|
72
|
+
**Returns:** `Promise<UserProfile>`
|
|
73
|
+
|
|
74
|
+
**Example:**
|
|
75
|
+
```typescript
|
|
76
|
+
const user = await qgate.register('alice');
|
|
77
|
+
console.log(user);
|
|
78
|
+
// { userId: "uuid", username: "alice", role: "user" }
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Note:** This generates and stores cryptographic keys locally. **Backup the seed!**
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
### `qgate.login()`
|
|
86
|
+
|
|
87
|
+
Authenticate using stored identity.
|
|
88
|
+
|
|
89
|
+
**Returns:** `Promise<string>` - Session token
|
|
90
|
+
|
|
91
|
+
**Example:**
|
|
92
|
+
```typescript
|
|
93
|
+
const token = await qgate.login();
|
|
94
|
+
// Use token for API requests
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Throws:**
|
|
98
|
+
- `NO_IDENTITY` - User must register first
|
|
99
|
+
- `MISSING_USER_ID` - Re-registration required
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
### `qgate.getProfile()`
|
|
104
|
+
|
|
105
|
+
Get current user's profile.
|
|
106
|
+
|
|
107
|
+
**Returns:** `Promise<UserProfile>`
|
|
108
|
+
|
|
109
|
+
**Example:**
|
|
110
|
+
```typescript
|
|
111
|
+
const profile = await qgate.getProfile();
|
|
112
|
+
console.log(profile);
|
|
113
|
+
// { userId: "uuid", username: "alice", role: "user", status: "active" }
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Requires:** Active session (must be logged in)
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
### `qgate.logout()`
|
|
121
|
+
|
|
122
|
+
End the current session.
|
|
123
|
+
|
|
124
|
+
**Returns:** `Promise<void>`
|
|
125
|
+
|
|
126
|
+
**Example:**
|
|
127
|
+
```typescript
|
|
128
|
+
await qgate.logout();
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Note:** Local keys are preserved for future login.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
### `qgate.hasIdentity()`
|
|
136
|
+
|
|
137
|
+
Check if user has registered identity.
|
|
138
|
+
|
|
139
|
+
**Returns:** `Promise<boolean>`
|
|
140
|
+
|
|
141
|
+
**Example:**
|
|
142
|
+
```typescript
|
|
143
|
+
if (await qgate.hasIdentity()) {
|
|
144
|
+
await qgate.login();
|
|
145
|
+
} else {
|
|
146
|
+
await qgate.register('alice');
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Advanced Usage
|
|
153
|
+
|
|
154
|
+
### Custom Storage
|
|
155
|
+
|
|
156
|
+
Provide your own storage implementation:
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
import { QGateClient, IStorage } from '@pqcloud/sdk';
|
|
160
|
+
|
|
161
|
+
class CustomStorage implements IStorage {
|
|
162
|
+
async getItem(key: string): Promise<string | null> {
|
|
163
|
+
// Your implementation
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async setItem(key: string, value: string): Promise<void> {
|
|
167
|
+
// Your implementation
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async removeItem(key: string): Promise<void> {
|
|
171
|
+
// Your implementation
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const qgate = new QGateClient({
|
|
176
|
+
apiKey: 'your-key',
|
|
177
|
+
storage: new CustomStorage()
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Error Handling
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
try {
|
|
185
|
+
await qgate.login();
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if (error.message === 'NO_IDENTITY') {
|
|
188
|
+
console.log('User not registered');
|
|
189
|
+
} else if (error.response?.status === 401) {
|
|
190
|
+
console.log('Authentication failed');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Session Management
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
// Check if user is logged in
|
|
199
|
+
const hasIdentity = await qgate.hasIdentity();
|
|
200
|
+
|
|
201
|
+
// Auto-login on app start
|
|
202
|
+
if (hasIdentity) {
|
|
203
|
+
try {
|
|
204
|
+
await qgate.login();
|
|
205
|
+
} catch {
|
|
206
|
+
// Handle expired/invalid session
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## TypeScript
|
|
212
|
+
|
|
213
|
+
Full TypeScript support included:
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
import { QGateClient, QGateConfig, UserProfile } from '@pqcloud/sdk';
|
|
217
|
+
|
|
218
|
+
const config: QGateConfig = {
|
|
219
|
+
apiKey: 'your-key'
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const qgate = new QGateClient(config);
|
|
223
|
+
|
|
224
|
+
const profile: UserProfile = await qgate.getProfile();
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Security Notes
|
|
228
|
+
|
|
229
|
+
🔒 **Private keys never leave the device**
|
|
230
|
+
🔒 **All communication is encrypted**
|
|
231
|
+
🔒 **Quantum-resistant cryptography**
|
|
232
|
+
🔒 **Store API keys securely (environment variables)**
|
|
233
|
+
|
|
234
|
+
## Examples
|
|
235
|
+
|
|
236
|
+
### React Integration
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
import { useState, useEffect } from 'react';
|
|
240
|
+
import { QGateClient } from '@pqcloud/sdk';
|
|
241
|
+
|
|
242
|
+
const qgate = new QGateClient({ apiKey: process.env.REACT_APP_QGATE_KEY });
|
|
243
|
+
|
|
244
|
+
function App() {
|
|
245
|
+
const [user, setUser] = useState(null);
|
|
246
|
+
|
|
247
|
+
useEffect(() => {
|
|
248
|
+
async function init() {
|
|
249
|
+
if (await qgate.hasIdentity()) {
|
|
250
|
+
await qgate.login();
|
|
251
|
+
const profile = await qgate.getProfile();
|
|
252
|
+
setUser(profile);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
init();
|
|
256
|
+
}, []);
|
|
257
|
+
|
|
258
|
+
const register = async (username) => {
|
|
259
|
+
const user = await qgate.register(username);
|
|
260
|
+
setUser(user);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const logout = async () => {
|
|
264
|
+
await qgate.logout();
|
|
265
|
+
setUser(null);
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
return user ? (
|
|
269
|
+
<div>
|
|
270
|
+
<h1>Welcome, {user.username}!</h1>
|
|
271
|
+
<button onClick={logout}>Logout</button>
|
|
272
|
+
</div>
|
|
273
|
+
) : (
|
|
274
|
+
<button onClick={() => register('alice')}>Register</button>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Node.js Backend
|
|
280
|
+
|
|
281
|
+
```javascript
|
|
282
|
+
const { QGateClient } = require('@pqcloud/sdk');
|
|
283
|
+
|
|
284
|
+
const qgate = new QGateClient({
|
|
285
|
+
apiKey: process.env.QGATE_API_KEY
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
async function authenticateUser(username) {
|
|
289
|
+
if (!await qgate.hasIdentity()) {
|
|
290
|
+
await qgate.register(username);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const token = await qgate.login();
|
|
294
|
+
return token;
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Support
|
|
299
|
+
|
|
300
|
+
**Documentation:** [Provider Guide](../PROVIDER_API_GUIDE.md)
|
|
301
|
+
**Issues:** [GitHub Issues](https://github.com/qgate/sdk/issues)
|
|
302
|
+
**Email:** support@qgate.io
|
|
303
|
+
|
|
304
|
+
## License
|
|
305
|
+
|
|
306
|
+
ISC
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
**Built with ❤️ by the Q-Gate Team**
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { IStorage } from './storage';
|
|
2
|
+
export interface QidConfig {
|
|
3
|
+
baseUrl?: string;
|
|
4
|
+
apiKey: string;
|
|
5
|
+
storage?: IStorage;
|
|
6
|
+
}
|
|
7
|
+
export interface UserProfile {
|
|
8
|
+
userId: string;
|
|
9
|
+
username: string;
|
|
10
|
+
role?: string;
|
|
11
|
+
plan?: string;
|
|
12
|
+
accountLimits?: {
|
|
13
|
+
maxProjects: number;
|
|
14
|
+
maxUsersTotal: number;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export declare class QidCloud {
|
|
18
|
+
private api;
|
|
19
|
+
private storage;
|
|
20
|
+
private config;
|
|
21
|
+
constructor(config: QidConfig);
|
|
22
|
+
hasIdentity(): Promise<boolean>;
|
|
23
|
+
register(username: string, tenantId?: string): Promise<UserProfile>;
|
|
24
|
+
/**
|
|
25
|
+
* Login / Authenticate
|
|
26
|
+
* Uses stored PQC keys to sign a challenge from the server
|
|
27
|
+
*/
|
|
28
|
+
login(): Promise<string>;
|
|
29
|
+
logout(): Promise<void>;
|
|
30
|
+
getProfile(): Promise<UserProfile>;
|
|
31
|
+
}
|
package/dist/QidCloud.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import axios from 'axios';
|
|
11
|
+
import * as PQC from './crypto/pqc';
|
|
12
|
+
import UnifiedStorage from './storage';
|
|
13
|
+
import { generateUUID, toBase64, fromBase64 } from './utils';
|
|
14
|
+
const DEFAULT_BACKEND_URL = 'https://qgate.onrender.com';
|
|
15
|
+
export class QidCloud {
|
|
16
|
+
constructor(config) {
|
|
17
|
+
this.config = Object.assign(Object.assign({}, config), { baseUrl: config.baseUrl || DEFAULT_BACKEND_URL });
|
|
18
|
+
this.storage = config.storage || UnifiedStorage;
|
|
19
|
+
this.api = axios.create({
|
|
20
|
+
baseURL: this.config.baseUrl,
|
|
21
|
+
headers: {
|
|
22
|
+
'Content-Type': 'application/json',
|
|
23
|
+
'X-API-KEY': config.apiKey
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
// Attach token interceptor
|
|
27
|
+
this.api.interceptors.request.use((req) => __awaiter(this, void 0, void 0, function* () {
|
|
28
|
+
const token = yield this.storage.getItem('qgate_token');
|
|
29
|
+
if (token) {
|
|
30
|
+
req.headers.Authorization = `Bearer ${token} `;
|
|
31
|
+
}
|
|
32
|
+
return req;
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
hasIdentity() {
|
|
36
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
37
|
+
const seed = yield this.storage.getItem('qgate_pqc_seed');
|
|
38
|
+
return !!seed;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
register(username_1) {
|
|
42
|
+
return __awaiter(this, arguments, void 0, function* (username, tenantId = 'default') {
|
|
43
|
+
// 1. Generate Seed
|
|
44
|
+
const seedHex = yield PQC.generateDilithiumSeed();
|
|
45
|
+
// 2. Persist Locally
|
|
46
|
+
yield this.storage.setItem('qgate_pqc_seed', seedHex);
|
|
47
|
+
// 3. Derive Keys
|
|
48
|
+
const keys = yield PQC.getKeysFromSeed(seedHex);
|
|
49
|
+
const initResp = yield this.api.post('/api/register/initiate', { tenantId });
|
|
50
|
+
const { regSessionId, regNonce } = initResp.data;
|
|
51
|
+
// 4. Sign Nonce
|
|
52
|
+
// Backend sends nonce as Base64
|
|
53
|
+
const nonceBytes = fromBase64(regNonce);
|
|
54
|
+
const signature = keys.sign(nonceBytes);
|
|
55
|
+
const signatureBase64 = toBase64(signature);
|
|
56
|
+
// 5. Complete Registration
|
|
57
|
+
const completeResp = yield this.api.post('/api/register/complete', {
|
|
58
|
+
regSessionId,
|
|
59
|
+
did: generateUUID(), // Device ID
|
|
60
|
+
username,
|
|
61
|
+
attestation: {
|
|
62
|
+
format: 'web-sdk',
|
|
63
|
+
signature: signatureBase64,
|
|
64
|
+
nonce: regNonce, // Echo back
|
|
65
|
+
publicKey: toBase64(keys.publicKey)
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
const { user, token } = completeResp.data;
|
|
69
|
+
// 6. Store Session
|
|
70
|
+
if (token) {
|
|
71
|
+
yield this.storage.setItem('qgate_token', token);
|
|
72
|
+
}
|
|
73
|
+
if (user && user.userId) {
|
|
74
|
+
yield this.storage.setItem('qgate_user_id', user.userId);
|
|
75
|
+
return user;
|
|
76
|
+
}
|
|
77
|
+
return user || { userId: 'unknown', username };
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Login / Authenticate
|
|
82
|
+
* Uses stored PQC keys to sign a challenge from the server
|
|
83
|
+
*/
|
|
84
|
+
login() {
|
|
85
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
86
|
+
if (!(yield this.hasIdentity())) {
|
|
87
|
+
throw new Error('NO_IDENTITY: Device not registered. Call register() first.');
|
|
88
|
+
}
|
|
89
|
+
const seedHex = yield this.storage.getItem('qgate_pqc_seed');
|
|
90
|
+
if (!seedHex) {
|
|
91
|
+
throw new Error('NO_IDENTITY_SEED: Seed not found. Re-register.');
|
|
92
|
+
}
|
|
93
|
+
const keys = yield PQC.getKeysFromSeed(seedHex);
|
|
94
|
+
// 1. Initiate Auth Challenge
|
|
95
|
+
let userId = yield this.storage.getItem('qgate_user_id');
|
|
96
|
+
if (!userId) {
|
|
97
|
+
// Try to find user from profile if logged in? No, we are logging in.
|
|
98
|
+
throw new Error('MISSING_USER_ID: Cannot look up user. Re-register.');
|
|
99
|
+
}
|
|
100
|
+
const initResp = yield this.api.post('/api/initiate', {
|
|
101
|
+
regUserId: userId,
|
|
102
|
+
clientHint: 'web-sdk'
|
|
103
|
+
});
|
|
104
|
+
const { sessionId, nonce } = initResp.data;
|
|
105
|
+
// 2. Sign Challenge
|
|
106
|
+
// Backend sends nonce as Base64
|
|
107
|
+
const nonceBytes = fromBase64(nonce);
|
|
108
|
+
const signature = keys.sign(nonceBytes);
|
|
109
|
+
const signatureBase64 = toBase64(signature);
|
|
110
|
+
// 3. Verify
|
|
111
|
+
const verifyResp = yield this.api.post('/api/verify', {
|
|
112
|
+
sessionId,
|
|
113
|
+
signature: signatureBase64
|
|
114
|
+
});
|
|
115
|
+
const { token } = verifyResp.data;
|
|
116
|
+
if (token) {
|
|
117
|
+
yield this.storage.setItem('qgate_token', token);
|
|
118
|
+
return token;
|
|
119
|
+
}
|
|
120
|
+
throw new Error('Authentication failed');
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
logout() {
|
|
124
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
125
|
+
try {
|
|
126
|
+
// Attempt to notify server
|
|
127
|
+
yield this.api.post('/api/logout');
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
console.warn('Logout API call failed', e);
|
|
131
|
+
}
|
|
132
|
+
yield this.storage.removeItem('qgate_token');
|
|
133
|
+
// Optional: Keep identity keys? Usually yes.
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
getProfile() {
|
|
137
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
138
|
+
const resp = yield this.api.get('/api/me');
|
|
139
|
+
return resp.data;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare function generateKyberKeyPair(): Promise<{
|
|
2
|
+
publicKey: any;
|
|
3
|
+
privateKey: any;
|
|
4
|
+
}>;
|
|
5
|
+
export declare function decapsulateSecret(privateKey: Uint8Array, cipherText: Uint8Array): Promise<any>;
|
|
6
|
+
export declare function generateDilithiumSeed(): Promise<string>;
|
|
7
|
+
export declare function getKeysFromSeed(seedHex: string): Promise<{
|
|
8
|
+
publicKey: Uint8Array<ArrayBufferLike>;
|
|
9
|
+
privateKey: Uint8Array<ArrayBufferLike>;
|
|
10
|
+
sign: (msg: Uint8Array) => Uint8Array<ArrayBufferLike>;
|
|
11
|
+
}>;
|
|
12
|
+
export declare function signMessage(seedHex: string, message: Uint8Array | string): Promise<Uint8Array<ArrayBufferLike>>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
// PQC Crypto Utility for Q-Gate SDK
|
|
11
|
+
import { DilithiumLevel, DilithiumKeyPair } from '@asanrom/dilithium';
|
|
12
|
+
import { Kyber1024 } from 'crystals-kyber-js';
|
|
13
|
+
import { hex2buf } from '../utils';
|
|
14
|
+
export function generateKyberKeyPair() {
|
|
15
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
16
|
+
// Hardcoded sizes for Kyber1024
|
|
17
|
+
const PK_SIZE = 1568;
|
|
18
|
+
const SK_SIZE = 3168;
|
|
19
|
+
// Assume Kyber1024 is a class we can instantiate or it has static methods.
|
|
20
|
+
// Based on library patterns, if static props were missing, maybe it's an instance.
|
|
21
|
+
const kyber = new Kyber1024();
|
|
22
|
+
// Try async generation first
|
|
23
|
+
try {
|
|
24
|
+
// @ts-ignore
|
|
25
|
+
if (kyber.generateKeyPair) {
|
|
26
|
+
// @ts-ignore
|
|
27
|
+
const [pk, sk] = yield kyber.generateKeyPair();
|
|
28
|
+
return { publicKey: pk, privateKey: sk };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch (e) { }
|
|
32
|
+
// Fallback to buffer passing
|
|
33
|
+
const pk = new Uint8Array(PK_SIZE);
|
|
34
|
+
const sk = new Uint8Array(SK_SIZE);
|
|
35
|
+
// @ts-ignore
|
|
36
|
+
kyber.keyPair(pk, sk);
|
|
37
|
+
return {
|
|
38
|
+
publicKey: pk,
|
|
39
|
+
privateKey: sk
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
export function decapsulateSecret(privateKey, cipherText) {
|
|
44
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
45
|
+
const kyber = new Kyber1024();
|
|
46
|
+
// @ts-ignore
|
|
47
|
+
if (kyber.decap) {
|
|
48
|
+
// @ts-ignore
|
|
49
|
+
return yield kyber.decap(cipherText, privateKey);
|
|
50
|
+
}
|
|
51
|
+
// Fallback
|
|
52
|
+
const SS_SIZE = 32;
|
|
53
|
+
const sharedSecret = new Uint8Array(SS_SIZE);
|
|
54
|
+
// @ts-ignore
|
|
55
|
+
kyber.decaps(sharedSecret, cipherText, privateKey);
|
|
56
|
+
return sharedSecret;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
export function generateDilithiumSeed() {
|
|
60
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
61
|
+
const seed = new Uint8Array(32);
|
|
62
|
+
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
63
|
+
crypto.getRandomValues(seed);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
for (let i = 0; i < 32; i++)
|
|
67
|
+
seed[i] = Math.floor(Math.random() * 256);
|
|
68
|
+
}
|
|
69
|
+
return Array.prototype.map.call(seed, (x) => ('00' + x.toString(16)).slice(-2)).join('');
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
export function getKeysFromSeed(seedHex) {
|
|
73
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
74
|
+
const level = DilithiumLevel.get(3);
|
|
75
|
+
const seed = hex2buf(seedHex);
|
|
76
|
+
const kp = DilithiumKeyPair.generate(level, seed);
|
|
77
|
+
return {
|
|
78
|
+
publicKey: kp.getPublicKey().getBytes(),
|
|
79
|
+
privateKey: kp.getPrivateKey().getBytes(),
|
|
80
|
+
// Helper to sign and return Uint8Array
|
|
81
|
+
sign: (msg) => kp.sign(msg).getBytes()
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
export function signMessage(seedHex, message) {
|
|
86
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
87
|
+
const keys = yield getKeysFromSeed(seedHex);
|
|
88
|
+
const msgBytes = typeof message === 'string' ? new TextEncoder().encode(message) : message;
|
|
89
|
+
return keys.sign(msgBytes);
|
|
90
|
+
});
|
|
91
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface IStorage {
|
|
2
|
+
getItem(key: string): Promise<string | null>;
|
|
3
|
+
setItem(key: string, value: string): Promise<void>;
|
|
4
|
+
removeItem(key: string): Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
export declare class MemoryStorage implements IStorage {
|
|
7
|
+
private storage;
|
|
8
|
+
getItem(key: string): Promise<string | null>;
|
|
9
|
+
setItem(key: string, value: string): Promise<void>;
|
|
10
|
+
removeItem(key: string): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
export declare class LocalStorageAdapter implements IStorage {
|
|
13
|
+
getItem(key: string): Promise<string | null>;
|
|
14
|
+
setItem(key: string, value: string): Promise<void>;
|
|
15
|
+
removeItem(key: string): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
declare const storage: MemoryStorage | LocalStorageAdapter;
|
|
18
|
+
export default storage;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
export class MemoryStorage {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.storage = new Map();
|
|
13
|
+
}
|
|
14
|
+
getItem(key) {
|
|
15
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
16
|
+
return this.storage.get(key) || null;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
setItem(key, value) {
|
|
20
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
21
|
+
this.storage.set(key, value);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
removeItem(key) {
|
|
25
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
26
|
+
this.storage.delete(key);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export class LocalStorageAdapter {
|
|
31
|
+
getItem(key) {
|
|
32
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
33
|
+
if (typeof window !== 'undefined' && window.localStorage) {
|
|
34
|
+
return window.localStorage.getItem(key);
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
setItem(key, value) {
|
|
40
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
41
|
+
if (typeof window !== 'undefined' && window.localStorage) {
|
|
42
|
+
window.localStorage.setItem(key, value);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
removeItem(key) {
|
|
47
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
48
|
+
if (typeof window !== 'undefined' && window.localStorage) {
|
|
49
|
+
window.localStorage.removeItem(key);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Default export: Auto-detect
|
|
55
|
+
const storage = typeof window !== 'undefined' && window.localStorage
|
|
56
|
+
? new LocalStorageAdapter()
|
|
57
|
+
: new MemoryStorage();
|
|
58
|
+
export default storage;
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function generateUUID(): string;
|
|
2
|
+
export declare function buf2hex(buffer: Uint8Array): string;
|
|
3
|
+
export declare function hex2buf(hex: string): Uint8Array;
|
|
4
|
+
export declare function toBase64(bytes: Uint8Array): string;
|
|
5
|
+
export declare function fromBase64(base64: string): Uint8Array;
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export function generateUUID() {
|
|
2
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
3
|
+
return crypto.randomUUID();
|
|
4
|
+
}
|
|
5
|
+
// Fallback for environments without randomUUID
|
|
6
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
7
|
+
var r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
8
|
+
return v.toString(16);
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
export function buf2hex(buffer) {
|
|
12
|
+
return Array.prototype.map.call(new Uint8Array(buffer), (x) => ('00' + x.toString(16)).slice(-2)).join('');
|
|
13
|
+
}
|
|
14
|
+
export function hex2buf(hex) {
|
|
15
|
+
if (!hex)
|
|
16
|
+
return new Uint8Array(0);
|
|
17
|
+
const bytes = new Uint8Array(Math.ceil(hex.length / 2));
|
|
18
|
+
for (let i = 0; i < bytes.length; i++)
|
|
19
|
+
bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
|
|
20
|
+
return bytes;
|
|
21
|
+
}
|
|
22
|
+
export function toBase64(bytes) {
|
|
23
|
+
if (typeof Buffer !== 'undefined') {
|
|
24
|
+
return Buffer.from(bytes).toString('base64');
|
|
25
|
+
}
|
|
26
|
+
// Browser fallback
|
|
27
|
+
let binary = '';
|
|
28
|
+
const len = bytes.byteLength;
|
|
29
|
+
for (let i = 0; i < len; i++) {
|
|
30
|
+
binary += String.fromCharCode(bytes[i]);
|
|
31
|
+
}
|
|
32
|
+
return window.btoa(binary);
|
|
33
|
+
}
|
|
34
|
+
export function fromBase64(base64) {
|
|
35
|
+
if (typeof Buffer !== 'undefined') {
|
|
36
|
+
return new Uint8Array(Buffer.from(base64, 'base64'));
|
|
37
|
+
}
|
|
38
|
+
// Browser fallback
|
|
39
|
+
const binary_string = window.atob(base64);
|
|
40
|
+
const len = binary_string.length;
|
|
41
|
+
const bytes = new Uint8Array(len);
|
|
42
|
+
for (let i = 0; i < len; i++) {
|
|
43
|
+
bytes[i] = binary_string.charCodeAt(i);
|
|
44
|
+
}
|
|
45
|
+
return bytes;
|
|
46
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@qidcloud/sdk",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Information-Theoretic Security SDK for Web",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"quantum",
|
|
13
|
+
"pqc",
|
|
14
|
+
"security",
|
|
15
|
+
"kyber",
|
|
16
|
+
"dilithium"
|
|
17
|
+
],
|
|
18
|
+
"author": "Q-Gate",
|
|
19
|
+
"license": "ISC",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@asanrom/dilithium": "^1.0.1",
|
|
22
|
+
"axios": "^1.6.0",
|
|
23
|
+
"crystals-kyber-js": "^1.1.1",
|
|
24
|
+
"eventemitter3": "^5.0.1",
|
|
25
|
+
"socket.io-client": "^4.7.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^20.19.27",
|
|
29
|
+
"typescript": "^5.0.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/QidCloud.ts
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import axios, { AxiosInstance } from 'axios';
|
|
2
|
+
import * as PQC from './crypto/pqc';
|
|
3
|
+
import UnifiedStorage, { IStorage } from './storage';
|
|
4
|
+
import { generateUUID, toBase64, hex2buf, fromBase64 } from './utils';
|
|
5
|
+
|
|
6
|
+
export interface QidConfig {
|
|
7
|
+
baseUrl?: string; // Optional, defaults to production backend
|
|
8
|
+
apiKey: string;
|
|
9
|
+
storage?: IStorage;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const DEFAULT_BACKEND_URL = 'https://qgate.onrender.com';
|
|
13
|
+
|
|
14
|
+
export interface UserProfile {
|
|
15
|
+
userId: string;
|
|
16
|
+
username: string;
|
|
17
|
+
role?: string;
|
|
18
|
+
plan?: string;
|
|
19
|
+
accountLimits?: {
|
|
20
|
+
maxProjects: number;
|
|
21
|
+
maxUsersTotal: number;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class QidCloud {
|
|
26
|
+
private api: AxiosInstance;
|
|
27
|
+
private storage: IStorage;
|
|
28
|
+
private config: QidConfig;
|
|
29
|
+
|
|
30
|
+
constructor(config: QidConfig) {
|
|
31
|
+
this.config = {
|
|
32
|
+
...config,
|
|
33
|
+
baseUrl: config.baseUrl || DEFAULT_BACKEND_URL
|
|
34
|
+
};
|
|
35
|
+
this.storage = config.storage || UnifiedStorage;
|
|
36
|
+
this.api = axios.create({
|
|
37
|
+
baseURL: this.config.baseUrl,
|
|
38
|
+
headers: {
|
|
39
|
+
'Content-Type': 'application/json',
|
|
40
|
+
'X-API-KEY': config.apiKey
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Attach token interceptor
|
|
45
|
+
this.api.interceptors.request.use(async (req) => {
|
|
46
|
+
const token = await this.storage.getItem('qgate_token');
|
|
47
|
+
if (token) {
|
|
48
|
+
req.headers.Authorization = `Bearer ${token} `;
|
|
49
|
+
}
|
|
50
|
+
return req;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async hasIdentity(): Promise<boolean> {
|
|
55
|
+
const seed = await this.storage.getItem('qgate_pqc_seed');
|
|
56
|
+
return !!seed;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async register(username: string, tenantId: string = 'default'): Promise<UserProfile> {
|
|
60
|
+
// 1. Generate Seed
|
|
61
|
+
const seedHex = await PQC.generateDilithiumSeed();
|
|
62
|
+
|
|
63
|
+
// 2. Persist Locally
|
|
64
|
+
await this.storage.setItem('qgate_pqc_seed', seedHex);
|
|
65
|
+
|
|
66
|
+
// 3. Derive Keys
|
|
67
|
+
const keys = await PQC.getKeysFromSeed(seedHex);
|
|
68
|
+
|
|
69
|
+
const initResp = await this.api.post('/api/register/initiate', { tenantId });
|
|
70
|
+
const { regSessionId, regNonce } = initResp.data;
|
|
71
|
+
|
|
72
|
+
// 4. Sign Nonce
|
|
73
|
+
// Backend sends nonce as Base64
|
|
74
|
+
const nonceBytes = fromBase64(regNonce);
|
|
75
|
+
const signature = keys.sign(nonceBytes);
|
|
76
|
+
const signatureBase64 = toBase64(signature);
|
|
77
|
+
|
|
78
|
+
// 5. Complete Registration
|
|
79
|
+
const completeResp = await this.api.post('/api/register/complete', {
|
|
80
|
+
regSessionId,
|
|
81
|
+
did: generateUUID(), // Device ID
|
|
82
|
+
username,
|
|
83
|
+
attestation: {
|
|
84
|
+
format: 'web-sdk',
|
|
85
|
+
signature: signatureBase64,
|
|
86
|
+
nonce: regNonce, // Echo back
|
|
87
|
+
publicKey: toBase64(keys.publicKey)
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const { user, token } = completeResp.data;
|
|
92
|
+
|
|
93
|
+
// 6. Store Session
|
|
94
|
+
if (token) {
|
|
95
|
+
await this.storage.setItem('qgate_token', token);
|
|
96
|
+
}
|
|
97
|
+
if (user && user.userId) {
|
|
98
|
+
await this.storage.setItem('qgate_user_id', user.userId);
|
|
99
|
+
return user;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return user || { userId: 'unknown', username };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Login / Authenticate
|
|
107
|
+
* Uses stored PQC keys to sign a challenge from the server
|
|
108
|
+
*/
|
|
109
|
+
async login(): Promise<string> {
|
|
110
|
+
if (!(await this.hasIdentity())) {
|
|
111
|
+
throw new Error('NO_IDENTITY: Device not registered. Call register() first.');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const seedHex = await this.storage.getItem('qgate_pqc_seed');
|
|
115
|
+
if (!seedHex) {
|
|
116
|
+
throw new Error('NO_IDENTITY_SEED: Seed not found. Re-register.');
|
|
117
|
+
}
|
|
118
|
+
const keys = await PQC.getKeysFromSeed(seedHex);
|
|
119
|
+
|
|
120
|
+
// 1. Initiate Auth Challenge
|
|
121
|
+
let userId = await this.storage.getItem('qgate_user_id');
|
|
122
|
+
|
|
123
|
+
if (!userId) {
|
|
124
|
+
// Try to find user from profile if logged in? No, we are logging in.
|
|
125
|
+
throw new Error('MISSING_USER_ID: Cannot look up user. Re-register.');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const initResp = await this.api.post('/api/initiate', {
|
|
129
|
+
regUserId: userId,
|
|
130
|
+
clientHint: 'web-sdk'
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const { sessionId, nonce } = initResp.data;
|
|
134
|
+
|
|
135
|
+
// 2. Sign Challenge
|
|
136
|
+
// Backend sends nonce as Base64
|
|
137
|
+
const nonceBytes = fromBase64(nonce);
|
|
138
|
+
const signature = keys.sign(nonceBytes);
|
|
139
|
+
const signatureBase64 = toBase64(signature);
|
|
140
|
+
|
|
141
|
+
// 3. Verify
|
|
142
|
+
const verifyResp = await this.api.post('/api/verify', {
|
|
143
|
+
sessionId,
|
|
144
|
+
signature: signatureBase64
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const { token } = verifyResp.data;
|
|
148
|
+
if (token) {
|
|
149
|
+
await this.storage.setItem('qgate_token', token);
|
|
150
|
+
return token;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
throw new Error('Authentication failed');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async logout(): Promise<void> {
|
|
157
|
+
try {
|
|
158
|
+
// Attempt to notify server
|
|
159
|
+
await this.api.post('/api/logout');
|
|
160
|
+
} catch (e) {
|
|
161
|
+
console.warn('Logout API call failed', e);
|
|
162
|
+
}
|
|
163
|
+
await this.storage.removeItem('qgate_token');
|
|
164
|
+
// Optional: Keep identity keys? Usually yes.
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async getProfile(): Promise<UserProfile> {
|
|
168
|
+
const resp = await this.api.get('/api/me');
|
|
169
|
+
return resp.data;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// PQC Crypto Utility for Q-Gate SDK
|
|
2
|
+
import { DilithiumLevel, DilithiumKeyPair } from '@asanrom/dilithium';
|
|
3
|
+
import { Kyber1024 } from 'crystals-kyber-js';
|
|
4
|
+
import { hex2buf } from '../utils';
|
|
5
|
+
|
|
6
|
+
export async function generateKyberKeyPair() {
|
|
7
|
+
// Hardcoded sizes for Kyber1024
|
|
8
|
+
const PK_SIZE = 1568;
|
|
9
|
+
const SK_SIZE = 3168;
|
|
10
|
+
|
|
11
|
+
// Assume Kyber1024 is a class we can instantiate or it has static methods.
|
|
12
|
+
// Based on library patterns, if static props were missing, maybe it's an instance.
|
|
13
|
+
const kyber = new Kyber1024();
|
|
14
|
+
// Try async generation first
|
|
15
|
+
try {
|
|
16
|
+
// @ts-ignore
|
|
17
|
+
if (kyber.generateKeyPair) {
|
|
18
|
+
// @ts-ignore
|
|
19
|
+
const [pk, sk] = await kyber.generateKeyPair();
|
|
20
|
+
return { publicKey: pk, privateKey: sk };
|
|
21
|
+
}
|
|
22
|
+
} catch (e) { }
|
|
23
|
+
|
|
24
|
+
// Fallback to buffer passing
|
|
25
|
+
const pk = new Uint8Array(PK_SIZE);
|
|
26
|
+
const sk = new Uint8Array(SK_SIZE);
|
|
27
|
+
// @ts-ignore
|
|
28
|
+
kyber.keyPair(pk, sk);
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
publicKey: pk,
|
|
32
|
+
privateKey: sk
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function decapsulateSecret(privateKey: Uint8Array, cipherText: Uint8Array) {
|
|
37
|
+
const kyber = new Kyber1024();
|
|
38
|
+
// @ts-ignore
|
|
39
|
+
if (kyber.decap) {
|
|
40
|
+
// @ts-ignore
|
|
41
|
+
return await kyber.decap(cipherText, privateKey);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Fallback
|
|
45
|
+
const SS_SIZE = 32;
|
|
46
|
+
const sharedSecret = new Uint8Array(SS_SIZE);
|
|
47
|
+
// @ts-ignore
|
|
48
|
+
kyber.decaps(sharedSecret, cipherText, privateKey);
|
|
49
|
+
return sharedSecret;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function generateDilithiumSeed(): Promise<string> {
|
|
53
|
+
const seed = new Uint8Array(32);
|
|
54
|
+
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
55
|
+
crypto.getRandomValues(seed);
|
|
56
|
+
} else {
|
|
57
|
+
for (let i = 0; i < 32; i++) seed[i] = Math.floor(Math.random() * 256);
|
|
58
|
+
}
|
|
59
|
+
return Array.prototype.map.call(seed, (x: number) => ('00' + x.toString(16)).slice(-2)).join('');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function getKeysFromSeed(seedHex: string) {
|
|
63
|
+
const level = DilithiumLevel.get(3);
|
|
64
|
+
const seed = hex2buf(seedHex);
|
|
65
|
+
|
|
66
|
+
const kp = DilithiumKeyPair.generate(level, seed);
|
|
67
|
+
return {
|
|
68
|
+
publicKey: kp.getPublicKey().getBytes(),
|
|
69
|
+
privateKey: kp.getPrivateKey().getBytes(),
|
|
70
|
+
// Helper to sign and return Uint8Array
|
|
71
|
+
sign: (msg: Uint8Array) => kp.sign(msg).getBytes()
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function signMessage(seedHex: string, message: Uint8Array | string) {
|
|
76
|
+
const keys = await getKeysFromSeed(seedHex);
|
|
77
|
+
const msgBytes = typeof message === 'string' ? new TextEncoder().encode(message) : message;
|
|
78
|
+
return keys.sign(msgBytes);
|
|
79
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export interface IStorage {
|
|
2
|
+
getItem(key: string): Promise<string | null>;
|
|
3
|
+
setItem(key: string, value: string): Promise<void>;
|
|
4
|
+
removeItem(key: string): Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class MemoryStorage implements IStorage {
|
|
8
|
+
private storage: Map<string, string> = new Map();
|
|
9
|
+
|
|
10
|
+
async getItem(key: string): Promise<string | null> {
|
|
11
|
+
return this.storage.get(key) || null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async setItem(key: string, value: string): Promise<void> {
|
|
15
|
+
this.storage.set(key, value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async removeItem(key: string): Promise<void> {
|
|
19
|
+
this.storage.delete(key);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class LocalStorageAdapter implements IStorage {
|
|
24
|
+
async getItem(key: string): Promise<string | null> {
|
|
25
|
+
if (typeof window !== 'undefined' && window.localStorage) {
|
|
26
|
+
return window.localStorage.getItem(key);
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async setItem(key: string, value: string): Promise<void> {
|
|
32
|
+
if (typeof window !== 'undefined' && window.localStorage) {
|
|
33
|
+
window.localStorage.setItem(key, value);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async removeItem(key: string): Promise<void> {
|
|
38
|
+
if (typeof window !== 'undefined' && window.localStorage) {
|
|
39
|
+
window.localStorage.removeItem(key);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Default export: Auto-detect
|
|
45
|
+
const storage = typeof window !== 'undefined' && window.localStorage
|
|
46
|
+
? new LocalStorageAdapter()
|
|
47
|
+
: new MemoryStorage();
|
|
48
|
+
|
|
49
|
+
export default storage;
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export function generateUUID(): string {
|
|
2
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
3
|
+
return crypto.randomUUID();
|
|
4
|
+
}
|
|
5
|
+
// Fallback for environments without randomUUID
|
|
6
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
7
|
+
var r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
8
|
+
return v.toString(16);
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function buf2hex(buffer: Uint8Array): string {
|
|
13
|
+
return Array.prototype.map.call(new Uint8Array(buffer), (x: number) => ('00' + x.toString(16)).slice(-2)).join('');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function hex2buf(hex: string): Uint8Array {
|
|
17
|
+
if (!hex) return new Uint8Array(0);
|
|
18
|
+
const bytes = new Uint8Array(Math.ceil(hex.length / 2));
|
|
19
|
+
for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
|
|
20
|
+
return bytes;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function toBase64(bytes: Uint8Array): string {
|
|
24
|
+
if (typeof Buffer !== 'undefined') {
|
|
25
|
+
return Buffer.from(bytes).toString('base64');
|
|
26
|
+
}
|
|
27
|
+
// Browser fallback
|
|
28
|
+
let binary = '';
|
|
29
|
+
const len = bytes.byteLength;
|
|
30
|
+
for (let i = 0; i < len; i++) {
|
|
31
|
+
binary += String.fromCharCode(bytes[i]);
|
|
32
|
+
}
|
|
33
|
+
return window.btoa(binary);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function fromBase64(base64: string): Uint8Array {
|
|
37
|
+
if (typeof Buffer !== 'undefined') {
|
|
38
|
+
return new Uint8Array(Buffer.from(base64, 'base64'));
|
|
39
|
+
}
|
|
40
|
+
// Browser fallback
|
|
41
|
+
const binary_string = window.atob(base64);
|
|
42
|
+
const len = binary_string.length;
|
|
43
|
+
const bytes = new Uint8Array(len);
|
|
44
|
+
for (let i = 0; i < len; i++) {
|
|
45
|
+
bytes[i] = binary_string.charCodeAt(i);
|
|
46
|
+
}
|
|
47
|
+
return bytes;
|
|
48
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES6",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": [
|
|
7
|
+
"DOM",
|
|
8
|
+
"ES2017"
|
|
9
|
+
],
|
|
10
|
+
"declaration": true,
|
|
11
|
+
"outDir": "./dist",
|
|
12
|
+
"strict": true,
|
|
13
|
+
"esModuleInterop": true,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"forceConsistentCasingInFileNames": true
|
|
16
|
+
},
|
|
17
|
+
"include": [
|
|
18
|
+
"src/**/*"
|
|
19
|
+
]
|
|
20
|
+
}
|
package/verify_sdk.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { QGateClient } from './src';
|
|
2
|
+
import * as PQC from './src/crypto/pqc';
|
|
3
|
+
|
|
4
|
+
async function verify() {
|
|
5
|
+
console.log('--- QGate SDK Verification ---');
|
|
6
|
+
|
|
7
|
+
// 1. Verify Exports
|
|
8
|
+
if (!QGateClient) {
|
|
9
|
+
throw new Error('QGateClient export missing');
|
|
10
|
+
}
|
|
11
|
+
console.log('✅ QGateClient exported');
|
|
12
|
+
|
|
13
|
+
// 2. Verify PQC Generation (Dilithium)
|
|
14
|
+
console.log('Testing Dilithium Seed Generation...');
|
|
15
|
+
const seed = await PQC.generateDilithiumSeed();
|
|
16
|
+
console.log('Generated Seed:', seed);
|
|
17
|
+
if (!seed || seed.length !== 64) { // 32 bytes hex
|
|
18
|
+
throw new Error('Invalid seed generated');
|
|
19
|
+
}
|
|
20
|
+
console.log('✅ Dilithium Seed Generated');
|
|
21
|
+
|
|
22
|
+
// 3. Verify Key Derivation
|
|
23
|
+
console.log('Deriving Keys...');
|
|
24
|
+
const keys = await PQC.getKeysFromSeed(seed);
|
|
25
|
+
if (!keys.publicKey || !keys.privateKey) {
|
|
26
|
+
throw new Error('Failed to derive keys');
|
|
27
|
+
}
|
|
28
|
+
console.log('✅ Keys Derived');
|
|
29
|
+
|
|
30
|
+
// 4. Verify Signing
|
|
31
|
+
console.log('Testing Signing...');
|
|
32
|
+
const msg = "Hello QGate";
|
|
33
|
+
const sig = keys.sign(new TextEncoder().encode(msg));
|
|
34
|
+
console.log('Signature Length:', sig.length);
|
|
35
|
+
if (sig.length === 0) {
|
|
36
|
+
throw new Error('Signature failed');
|
|
37
|
+
}
|
|
38
|
+
console.log('✅ Signing Success'); // Verification requires Dilithium library verify which we didn't export in PQC yet but sign works.
|
|
39
|
+
|
|
40
|
+
// 5. Verify Client Instantiation
|
|
41
|
+
const client = new QGateClient({
|
|
42
|
+
baseUrl: 'http://localhost:3000',
|
|
43
|
+
apiKey: 'test-key'
|
|
44
|
+
});
|
|
45
|
+
console.log('✅ Client Instantiated');
|
|
46
|
+
|
|
47
|
+
console.log('--- SDK VERIFIED ---');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
verify().catch(err => {
|
|
51
|
+
console.error('❌ VERIFICATION FAILED:', err);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
});
|