@phantom/indexed-db-stamper 0.1.1
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 +165 -0
- package/dist/index.d.ts +69 -0
- package/dist/index.js +236 -0
- package/dist/index.mjs +205 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# @phantom/indexed-db-stamper
|
|
2
|
+
|
|
3
|
+
A secure IndexedDB-based key stamper for the Phantom Wallet SDK that stores cryptographic keys directly in the browser's IndexedDB without ever exposing private key material.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Maximum Security**: Uses non-extractable Ed25519 keys that never exist in JavaScript memory
|
|
8
|
+
- **Web Crypto API**: Leverages browser's native cryptographic secure context
|
|
9
|
+
- **Secure Storage**: Keys stored as non-extractable CryptoKey objects in IndexedDB
|
|
10
|
+
- **Raw Signatures**: Uses Ed25519 raw signature format for maximum efficiency
|
|
11
|
+
- **Hardware Integration**: Utilizes browser's hardware-backed cryptographic isolation when available
|
|
12
|
+
- **Compatible Interface**: Drop-in replacement for other stamper implementations
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @phantom/indexed-db-stamper
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
### Basic Usage
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { IndexedDbStamper } from '@phantom/indexed-db-stamper';
|
|
26
|
+
|
|
27
|
+
// Create stamper instance
|
|
28
|
+
const stamper = new IndexedDbStamper({
|
|
29
|
+
dbName: 'my-app-keys', // optional, defaults to 'phantom-indexed-db-stamper'
|
|
30
|
+
storeName: 'crypto-keys', // optional, defaults to 'crypto-keys'
|
|
31
|
+
keyName: 'signing-key' // optional, defaults to 'signing-key'
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Initialize and generate/load keys
|
|
35
|
+
const keyInfo = await stamper.init();
|
|
36
|
+
console.log('Key ID:', keyInfo.keyId);
|
|
37
|
+
console.log('Public Key:', keyInfo.publicKey);
|
|
38
|
+
|
|
39
|
+
// Create X-Phantom-Stamp header value for API requests
|
|
40
|
+
const requestData = Buffer.from(JSON.stringify({ action: 'transfer', amount: 100 }), 'utf8');
|
|
41
|
+
const stamp = await stamper.stamp({ data: requestData });
|
|
42
|
+
console.log('X-Phantom-Stamp:', stamp);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Advanced Usage
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
// Check if already initialized
|
|
49
|
+
if (stamper.getKeyInfo()) {
|
|
50
|
+
console.log('Stamper already has keys');
|
|
51
|
+
} else {
|
|
52
|
+
await stamper.init();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Reset keys (generate new keypair)
|
|
56
|
+
const newKeyInfo = await stamper.resetKeyPair();
|
|
57
|
+
|
|
58
|
+
// Stamp different data types with PKI (default)
|
|
59
|
+
const stringData = Buffer.from('string data', 'utf8');
|
|
60
|
+
const binaryData = Buffer.from([1, 2, 3]);
|
|
61
|
+
const jsonData = Buffer.from(JSON.stringify({ key: 'value' }), 'utf8');
|
|
62
|
+
|
|
63
|
+
await stamper.stamp({ data: stringData });
|
|
64
|
+
await stamper.stamp({ data: binaryData, type: 'PKI' }); // explicit PKI type
|
|
65
|
+
await stamper.stamp({ data: jsonData });
|
|
66
|
+
|
|
67
|
+
// OIDC type stamping (requires idToken and salt)
|
|
68
|
+
const oidcStamp = await stamper.stamp({
|
|
69
|
+
data: requestData,
|
|
70
|
+
type: 'OIDC',
|
|
71
|
+
idToken: 'your-id-token',
|
|
72
|
+
salt: 'your-salt-value'
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Clear all stored keys
|
|
76
|
+
await stamper.clear();
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## API Reference
|
|
80
|
+
|
|
81
|
+
### Constructor
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
new IndexedDbStamper(config?: IndexedDbStamperConfig)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Config Options:**
|
|
88
|
+
- `dbName?: string` - IndexedDB database name (default: 'phantom-indexed-db-stamper')
|
|
89
|
+
- `storeName?: string` - Object store name (default: 'crypto-keys')
|
|
90
|
+
- `keyName?: string` - Key identifier prefix (default: 'signing-key')
|
|
91
|
+
|
|
92
|
+
### Methods
|
|
93
|
+
|
|
94
|
+
#### `init(): Promise<StamperKeyInfo>`
|
|
95
|
+
Initialize the stamper and generate/load cryptographic keys.
|
|
96
|
+
|
|
97
|
+
**Returns:** `StamperKeyInfo` with `keyId` and `publicKey`
|
|
98
|
+
|
|
99
|
+
#### `getKeyInfo(): StamperKeyInfo | null`
|
|
100
|
+
Get current key information without async operation.
|
|
101
|
+
|
|
102
|
+
#### `resetKeyPair(): Promise<StamperKeyInfo>`
|
|
103
|
+
Generate and store a new key pair, replacing any existing keys.
|
|
104
|
+
|
|
105
|
+
#### `stamp(params: { data: Buffer; type?: 'PKI'; idToken?: never; salt?: never; } | { data: Buffer; type: 'OIDC'; idToken: string; salt: string; }): Promise<string>`
|
|
106
|
+
Create X-Phantom-Stamp header value using the stored private key.
|
|
107
|
+
|
|
108
|
+
**Parameters:**
|
|
109
|
+
- `params.data: Buffer` - Data to sign (typically JSON stringified request body)
|
|
110
|
+
- `params.type?: 'PKI' | 'OIDC'` - Stamp type (defaults to 'PKI')
|
|
111
|
+
- `params.idToken?: string` - Required for OIDC type
|
|
112
|
+
- `params.salt?: string` - Required for OIDC type
|
|
113
|
+
|
|
114
|
+
**Returns:** Complete X-Phantom-Stamp header value (base64url-encoded JSON with base64url-encoded publicKey, signature, and kind fields)
|
|
115
|
+
|
|
116
|
+
**Note:** The public key is stored internally in base58 format but converted to base64url when creating stamps for API compatibility.
|
|
117
|
+
|
|
118
|
+
#### `clear(): Promise<void>`
|
|
119
|
+
Remove all stored keys from IndexedDB.
|
|
120
|
+
|
|
121
|
+
## Security Features
|
|
122
|
+
|
|
123
|
+
### Non-Extractable Keys
|
|
124
|
+
The stamper generates Ed25519 CryptoKey objects with `extractable: false`, meaning private keys cannot be exported, extracted, or accessed outside of Web Crypto API signing operations. This provides the strongest possible security in browser environments.
|
|
125
|
+
|
|
126
|
+
### Cryptographic Isolation
|
|
127
|
+
Keys are generated and stored entirely within the browser's secure cryptographic context:
|
|
128
|
+
- Private keys never exist in JavaScript memory at any point
|
|
129
|
+
- Signing operations happen within Web Crypto API secure boundaries
|
|
130
|
+
- Secure elements used when available by the browser
|
|
131
|
+
- Origin-based security isolation through IndexedDB
|
|
132
|
+
|
|
133
|
+
### Signature Format
|
|
134
|
+
The stamper uses Ed25519 signatures in their native 64-byte format, providing efficient and secure signing operations.
|
|
135
|
+
|
|
136
|
+
## Error Handling
|
|
137
|
+
|
|
138
|
+
The stamper includes comprehensive error handling for:
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
// Environment validation
|
|
142
|
+
if (typeof window === 'undefined') {
|
|
143
|
+
throw new Error('IndexedDbStamper requires a browser environment');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Initialization checks
|
|
147
|
+
if (!stamper.getKeyInfo()) {
|
|
148
|
+
throw new Error('Stamper not initialized. Call init() first.');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Storage errors
|
|
152
|
+
try {
|
|
153
|
+
await stamper.init();
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error('Failed to initialize stamper:', error);
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Browser Compatibility
|
|
160
|
+
|
|
161
|
+
Requires IndexedDB and Web Crypto API support.
|
|
162
|
+
|
|
163
|
+
## License
|
|
164
|
+
|
|
165
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Buffer } from 'buffer';
|
|
2
|
+
import { StamperWithKeyManagement, Algorithm, StamperKeyInfo } from '@phantom/sdk-types';
|
|
3
|
+
|
|
4
|
+
type IndexedDbStamperConfig = {
|
|
5
|
+
dbName?: string;
|
|
6
|
+
storeName?: string;
|
|
7
|
+
keyName?: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* IndexedDB-based key manager that stores cryptographic keys securely in IndexedDB
|
|
11
|
+
* and performs signing operations without ever exposing private key material.
|
|
12
|
+
*
|
|
13
|
+
* Security model:
|
|
14
|
+
* - Generates non-extractable Ed25519 keypairs using Web Crypto API
|
|
15
|
+
* - Stores keys entirely within Web Crypto API secure context
|
|
16
|
+
* - Private keys NEVER exist in JavaScript memory
|
|
17
|
+
* - Provides signing methods without exposing private keys
|
|
18
|
+
* - Maximum security using browser's native cryptographic isolation
|
|
19
|
+
*/
|
|
20
|
+
declare class IndexedDbStamper implements StamperWithKeyManagement {
|
|
21
|
+
private dbName;
|
|
22
|
+
private storeName;
|
|
23
|
+
private keyName;
|
|
24
|
+
private db;
|
|
25
|
+
private keyInfo;
|
|
26
|
+
private cryptoKeyPair;
|
|
27
|
+
algorithm: Algorithm;
|
|
28
|
+
constructor(config?: IndexedDbStamperConfig);
|
|
29
|
+
/**
|
|
30
|
+
* Initialize the stamper by opening IndexedDB and retrieving or generating keys
|
|
31
|
+
*/
|
|
32
|
+
init(): Promise<StamperKeyInfo>;
|
|
33
|
+
/**
|
|
34
|
+
* Get the public key information
|
|
35
|
+
*/
|
|
36
|
+
getKeyInfo(): StamperKeyInfo | null;
|
|
37
|
+
/**
|
|
38
|
+
* Reset the key pair by generating a new one
|
|
39
|
+
*/
|
|
40
|
+
resetKeyPair(): Promise<StamperKeyInfo>;
|
|
41
|
+
/**
|
|
42
|
+
* Create X-Phantom-Stamp header value using stored private key
|
|
43
|
+
* @param params - Parameters object with data and optional type/options
|
|
44
|
+
* @returns Complete X-Phantom-Stamp header value
|
|
45
|
+
*/
|
|
46
|
+
stamp(params: {
|
|
47
|
+
data: Buffer;
|
|
48
|
+
type?: 'PKI';
|
|
49
|
+
idToken?: never;
|
|
50
|
+
salt?: never;
|
|
51
|
+
} | {
|
|
52
|
+
data: Buffer;
|
|
53
|
+
type: 'OIDC';
|
|
54
|
+
idToken: string;
|
|
55
|
+
salt: string;
|
|
56
|
+
}): Promise<string>;
|
|
57
|
+
/**
|
|
58
|
+
* Clear all stored keys
|
|
59
|
+
*/
|
|
60
|
+
clear(): Promise<void>;
|
|
61
|
+
private clearStoredKeys;
|
|
62
|
+
private openDB;
|
|
63
|
+
private generateAndStoreKeyPair;
|
|
64
|
+
private storeKeyPair;
|
|
65
|
+
private loadKeyPair;
|
|
66
|
+
private getStoredKeyInfo;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export { IndexedDbStamper, IndexedDbStamperConfig };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
IndexedDbStamper: () => IndexedDbStamper
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(src_exports);
|
|
36
|
+
var import_base64url = require("@phantom/base64url");
|
|
37
|
+
var import_bs58 = __toESM(require("bs58"));
|
|
38
|
+
var import_sdk_types = require("@phantom/sdk-types");
|
|
39
|
+
var IndexedDbStamper = class {
|
|
40
|
+
// Use Ed25519 for maximum security and performance
|
|
41
|
+
constructor(config = {}) {
|
|
42
|
+
this.db = null;
|
|
43
|
+
this.keyInfo = null;
|
|
44
|
+
this.cryptoKeyPair = null;
|
|
45
|
+
this.algorithm = import_sdk_types.Algorithm.ed25519;
|
|
46
|
+
if (typeof window === "undefined" || !window.indexedDB) {
|
|
47
|
+
throw new Error("IndexedDbStamper requires a browser environment with IndexedDB support");
|
|
48
|
+
}
|
|
49
|
+
this.dbName = config.dbName || "phantom-indexed-db-stamper";
|
|
50
|
+
this.storeName = config.storeName || "crypto-keys";
|
|
51
|
+
this.keyName = config.keyName || "signing-key";
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Initialize the stamper by opening IndexedDB and retrieving or generating keys
|
|
55
|
+
*/
|
|
56
|
+
async init() {
|
|
57
|
+
await this.openDB();
|
|
58
|
+
let keyInfo = await this.getStoredKeyInfo();
|
|
59
|
+
if (!keyInfo) {
|
|
60
|
+
keyInfo = await this.generateAndStoreKeyPair();
|
|
61
|
+
} else {
|
|
62
|
+
await this.loadKeyPair();
|
|
63
|
+
}
|
|
64
|
+
this.keyInfo = keyInfo;
|
|
65
|
+
return keyInfo;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Get the public key information
|
|
69
|
+
*/
|
|
70
|
+
getKeyInfo() {
|
|
71
|
+
return this.keyInfo;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Reset the key pair by generating a new one
|
|
75
|
+
*/
|
|
76
|
+
async resetKeyPair() {
|
|
77
|
+
await this.clearStoredKeys();
|
|
78
|
+
const keyInfo = await this.generateAndStoreKeyPair();
|
|
79
|
+
this.keyInfo = keyInfo;
|
|
80
|
+
return keyInfo;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Create X-Phantom-Stamp header value using stored private key
|
|
84
|
+
* @param params - Parameters object with data and optional type/options
|
|
85
|
+
* @returns Complete X-Phantom-Stamp header value
|
|
86
|
+
*/
|
|
87
|
+
async stamp(params) {
|
|
88
|
+
const { data, type = "PKI" } = params;
|
|
89
|
+
if (!this.keyInfo || !this.cryptoKeyPair) {
|
|
90
|
+
throw new Error("Stamper not initialized. Call init() first.");
|
|
91
|
+
}
|
|
92
|
+
const dataBytes = new Uint8Array(data);
|
|
93
|
+
const signature = await crypto.subtle.sign(
|
|
94
|
+
{
|
|
95
|
+
name: this.algorithm,
|
|
96
|
+
hash: "SHA-256"
|
|
97
|
+
},
|
|
98
|
+
this.cryptoKeyPair.privateKey,
|
|
99
|
+
dataBytes
|
|
100
|
+
);
|
|
101
|
+
const signatureBase64url = (0, import_base64url.base64urlEncode)(new Uint8Array(signature));
|
|
102
|
+
const stampData = type === "PKI" ? {
|
|
103
|
+
// Decode base58 public key to bytes, then encode as base64url (consistent with ApiKeyStamper)
|
|
104
|
+
publicKey: (0, import_base64url.base64urlEncode)(import_bs58.default.decode(this.keyInfo.publicKey)),
|
|
105
|
+
signature: signatureBase64url,
|
|
106
|
+
kind: "PKI",
|
|
107
|
+
algorithm: this.algorithm
|
|
108
|
+
} : {
|
|
109
|
+
kind: "OIDC",
|
|
110
|
+
idToken: params.idToken,
|
|
111
|
+
publicKey: (0, import_base64url.base64urlEncode)(import_bs58.default.decode(this.keyInfo.publicKey)),
|
|
112
|
+
salt: params.salt,
|
|
113
|
+
algorithm: this.algorithm,
|
|
114
|
+
signature: signatureBase64url
|
|
115
|
+
};
|
|
116
|
+
const stampJson = JSON.stringify(stampData);
|
|
117
|
+
return (0, import_base64url.base64urlEncode)(stampJson);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Clear all stored keys
|
|
121
|
+
*/
|
|
122
|
+
async clear() {
|
|
123
|
+
await this.clearStoredKeys();
|
|
124
|
+
this.keyInfo = null;
|
|
125
|
+
this.cryptoKeyPair = null;
|
|
126
|
+
}
|
|
127
|
+
async clearStoredKeys() {
|
|
128
|
+
if (!this.db) {
|
|
129
|
+
await this.openDB();
|
|
130
|
+
}
|
|
131
|
+
return new Promise((resolve, reject) => {
|
|
132
|
+
const transaction = this.db.transaction([this.storeName], "readwrite");
|
|
133
|
+
const store = transaction.objectStore(this.storeName);
|
|
134
|
+
const deleteKeyPair = store.delete(`${this.keyName}-keypair`);
|
|
135
|
+
const deleteKeyInfo = store.delete(`${this.keyName}-info`);
|
|
136
|
+
let completed = 0;
|
|
137
|
+
const total = 2;
|
|
138
|
+
const checkComplete = () => {
|
|
139
|
+
completed++;
|
|
140
|
+
if (completed === total) {
|
|
141
|
+
resolve();
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
deleteKeyPair.onsuccess = checkComplete;
|
|
145
|
+
deleteKeyInfo.onsuccess = checkComplete;
|
|
146
|
+
deleteKeyPair.onerror = () => reject(deleteKeyPair.error);
|
|
147
|
+
deleteKeyInfo.onerror = () => reject(deleteKeyInfo.error);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
async openDB() {
|
|
151
|
+
return new Promise((resolve, reject) => {
|
|
152
|
+
const request = indexedDB.open(this.dbName, 1);
|
|
153
|
+
request.onerror = () => reject(request.error);
|
|
154
|
+
request.onsuccess = () => {
|
|
155
|
+
this.db = request.result;
|
|
156
|
+
resolve();
|
|
157
|
+
};
|
|
158
|
+
request.onupgradeneeded = (event) => {
|
|
159
|
+
const db = event.target.result;
|
|
160
|
+
if (!db.objectStoreNames.contains(this.storeName)) {
|
|
161
|
+
db.createObjectStore(this.storeName);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
async generateAndStoreKeyPair() {
|
|
167
|
+
this.cryptoKeyPair = await crypto.subtle.generateKey(
|
|
168
|
+
{
|
|
169
|
+
name: "Ed25519"
|
|
170
|
+
},
|
|
171
|
+
false,
|
|
172
|
+
// non-extractable - private key can never be exported
|
|
173
|
+
["sign", "verify"]
|
|
174
|
+
);
|
|
175
|
+
const rawPublicKeyBuffer = await crypto.subtle.exportKey("raw", this.cryptoKeyPair.publicKey);
|
|
176
|
+
const publicKeyBase58 = import_bs58.default.encode(new Uint8Array(rawPublicKeyBuffer));
|
|
177
|
+
const keyIdBuffer = await crypto.subtle.digest("SHA-256", rawPublicKeyBuffer);
|
|
178
|
+
const keyId = (0, import_base64url.base64urlEncode)(new Uint8Array(keyIdBuffer)).substring(0, 16);
|
|
179
|
+
const keyInfo = {
|
|
180
|
+
keyId,
|
|
181
|
+
publicKey: publicKeyBase58
|
|
182
|
+
};
|
|
183
|
+
await this.storeKeyPair(this.cryptoKeyPair, keyInfo);
|
|
184
|
+
return keyInfo;
|
|
185
|
+
}
|
|
186
|
+
async storeKeyPair(keyPair, keyInfo) {
|
|
187
|
+
if (!this.db) {
|
|
188
|
+
throw new Error("Database not initialized");
|
|
189
|
+
}
|
|
190
|
+
return new Promise((resolve, reject) => {
|
|
191
|
+
const transaction = this.db.transaction([this.storeName], "readwrite");
|
|
192
|
+
const store = transaction.objectStore(this.storeName);
|
|
193
|
+
const keyPairRequest = store.put(keyPair, `${this.keyName}-keypair`);
|
|
194
|
+
const keyInfoRequest = store.put(keyInfo, `${this.keyName}-info`);
|
|
195
|
+
let completed = 0;
|
|
196
|
+
const total = 2;
|
|
197
|
+
const checkComplete = () => {
|
|
198
|
+
completed++;
|
|
199
|
+
if (completed === total) {
|
|
200
|
+
resolve();
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
keyPairRequest.onsuccess = checkComplete;
|
|
204
|
+
keyInfoRequest.onsuccess = checkComplete;
|
|
205
|
+
keyPairRequest.onerror = () => reject(keyPairRequest.error);
|
|
206
|
+
keyInfoRequest.onerror = () => reject(keyInfoRequest.error);
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
async loadKeyPair() {
|
|
210
|
+
if (!this.db) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
return new Promise((resolve, reject) => {
|
|
214
|
+
const transaction = this.db.transaction([this.storeName], "readonly");
|
|
215
|
+
const store = transaction.objectStore(this.storeName);
|
|
216
|
+
const request = store.get(`${this.keyName}-keypair`);
|
|
217
|
+
request.onsuccess = () => {
|
|
218
|
+
this.cryptoKeyPair = request.result || null;
|
|
219
|
+
resolve();
|
|
220
|
+
};
|
|
221
|
+
request.onerror = () => reject(request.error);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
async getStoredKeyInfo() {
|
|
225
|
+
if (!this.db) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
return new Promise((resolve, reject) => {
|
|
229
|
+
const transaction = this.db.transaction([this.storeName], "readonly");
|
|
230
|
+
const store = transaction.objectStore(this.storeName);
|
|
231
|
+
const request = store.get(`${this.keyName}-info`);
|
|
232
|
+
request.onsuccess = () => resolve(request.result || null);
|
|
233
|
+
request.onerror = () => reject(request.error);
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
};
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { base64urlEncode } from "@phantom/base64url";
|
|
3
|
+
import bs58 from "bs58";
|
|
4
|
+
import { Algorithm } from "@phantom/sdk-types";
|
|
5
|
+
var IndexedDbStamper = class {
|
|
6
|
+
// Use Ed25519 for maximum security and performance
|
|
7
|
+
constructor(config = {}) {
|
|
8
|
+
this.db = null;
|
|
9
|
+
this.keyInfo = null;
|
|
10
|
+
this.cryptoKeyPair = null;
|
|
11
|
+
this.algorithm = Algorithm.ed25519;
|
|
12
|
+
if (typeof window === "undefined" || !window.indexedDB) {
|
|
13
|
+
throw new Error("IndexedDbStamper requires a browser environment with IndexedDB support");
|
|
14
|
+
}
|
|
15
|
+
this.dbName = config.dbName || "phantom-indexed-db-stamper";
|
|
16
|
+
this.storeName = config.storeName || "crypto-keys";
|
|
17
|
+
this.keyName = config.keyName || "signing-key";
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Initialize the stamper by opening IndexedDB and retrieving or generating keys
|
|
21
|
+
*/
|
|
22
|
+
async init() {
|
|
23
|
+
await this.openDB();
|
|
24
|
+
let keyInfo = await this.getStoredKeyInfo();
|
|
25
|
+
if (!keyInfo) {
|
|
26
|
+
keyInfo = await this.generateAndStoreKeyPair();
|
|
27
|
+
} else {
|
|
28
|
+
await this.loadKeyPair();
|
|
29
|
+
}
|
|
30
|
+
this.keyInfo = keyInfo;
|
|
31
|
+
return keyInfo;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Get the public key information
|
|
35
|
+
*/
|
|
36
|
+
getKeyInfo() {
|
|
37
|
+
return this.keyInfo;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Reset the key pair by generating a new one
|
|
41
|
+
*/
|
|
42
|
+
async resetKeyPair() {
|
|
43
|
+
await this.clearStoredKeys();
|
|
44
|
+
const keyInfo = await this.generateAndStoreKeyPair();
|
|
45
|
+
this.keyInfo = keyInfo;
|
|
46
|
+
return keyInfo;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Create X-Phantom-Stamp header value using stored private key
|
|
50
|
+
* @param params - Parameters object with data and optional type/options
|
|
51
|
+
* @returns Complete X-Phantom-Stamp header value
|
|
52
|
+
*/
|
|
53
|
+
async stamp(params) {
|
|
54
|
+
const { data, type = "PKI" } = params;
|
|
55
|
+
if (!this.keyInfo || !this.cryptoKeyPair) {
|
|
56
|
+
throw new Error("Stamper not initialized. Call init() first.");
|
|
57
|
+
}
|
|
58
|
+
const dataBytes = new Uint8Array(data);
|
|
59
|
+
const signature = await crypto.subtle.sign(
|
|
60
|
+
{
|
|
61
|
+
name: this.algorithm,
|
|
62
|
+
hash: "SHA-256"
|
|
63
|
+
},
|
|
64
|
+
this.cryptoKeyPair.privateKey,
|
|
65
|
+
dataBytes
|
|
66
|
+
);
|
|
67
|
+
const signatureBase64url = base64urlEncode(new Uint8Array(signature));
|
|
68
|
+
const stampData = type === "PKI" ? {
|
|
69
|
+
// Decode base58 public key to bytes, then encode as base64url (consistent with ApiKeyStamper)
|
|
70
|
+
publicKey: base64urlEncode(bs58.decode(this.keyInfo.publicKey)),
|
|
71
|
+
signature: signatureBase64url,
|
|
72
|
+
kind: "PKI",
|
|
73
|
+
algorithm: this.algorithm
|
|
74
|
+
} : {
|
|
75
|
+
kind: "OIDC",
|
|
76
|
+
idToken: params.idToken,
|
|
77
|
+
publicKey: base64urlEncode(bs58.decode(this.keyInfo.publicKey)),
|
|
78
|
+
salt: params.salt,
|
|
79
|
+
algorithm: this.algorithm,
|
|
80
|
+
signature: signatureBase64url
|
|
81
|
+
};
|
|
82
|
+
const stampJson = JSON.stringify(stampData);
|
|
83
|
+
return base64urlEncode(stampJson);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Clear all stored keys
|
|
87
|
+
*/
|
|
88
|
+
async clear() {
|
|
89
|
+
await this.clearStoredKeys();
|
|
90
|
+
this.keyInfo = null;
|
|
91
|
+
this.cryptoKeyPair = null;
|
|
92
|
+
}
|
|
93
|
+
async clearStoredKeys() {
|
|
94
|
+
if (!this.db) {
|
|
95
|
+
await this.openDB();
|
|
96
|
+
}
|
|
97
|
+
return new Promise((resolve, reject) => {
|
|
98
|
+
const transaction = this.db.transaction([this.storeName], "readwrite");
|
|
99
|
+
const store = transaction.objectStore(this.storeName);
|
|
100
|
+
const deleteKeyPair = store.delete(`${this.keyName}-keypair`);
|
|
101
|
+
const deleteKeyInfo = store.delete(`${this.keyName}-info`);
|
|
102
|
+
let completed = 0;
|
|
103
|
+
const total = 2;
|
|
104
|
+
const checkComplete = () => {
|
|
105
|
+
completed++;
|
|
106
|
+
if (completed === total) {
|
|
107
|
+
resolve();
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
deleteKeyPair.onsuccess = checkComplete;
|
|
111
|
+
deleteKeyInfo.onsuccess = checkComplete;
|
|
112
|
+
deleteKeyPair.onerror = () => reject(deleteKeyPair.error);
|
|
113
|
+
deleteKeyInfo.onerror = () => reject(deleteKeyInfo.error);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
async openDB() {
|
|
117
|
+
return new Promise((resolve, reject) => {
|
|
118
|
+
const request = indexedDB.open(this.dbName, 1);
|
|
119
|
+
request.onerror = () => reject(request.error);
|
|
120
|
+
request.onsuccess = () => {
|
|
121
|
+
this.db = request.result;
|
|
122
|
+
resolve();
|
|
123
|
+
};
|
|
124
|
+
request.onupgradeneeded = (event) => {
|
|
125
|
+
const db = event.target.result;
|
|
126
|
+
if (!db.objectStoreNames.contains(this.storeName)) {
|
|
127
|
+
db.createObjectStore(this.storeName);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
async generateAndStoreKeyPair() {
|
|
133
|
+
this.cryptoKeyPair = await crypto.subtle.generateKey(
|
|
134
|
+
{
|
|
135
|
+
name: "Ed25519"
|
|
136
|
+
},
|
|
137
|
+
false,
|
|
138
|
+
// non-extractable - private key can never be exported
|
|
139
|
+
["sign", "verify"]
|
|
140
|
+
);
|
|
141
|
+
const rawPublicKeyBuffer = await crypto.subtle.exportKey("raw", this.cryptoKeyPair.publicKey);
|
|
142
|
+
const publicKeyBase58 = bs58.encode(new Uint8Array(rawPublicKeyBuffer));
|
|
143
|
+
const keyIdBuffer = await crypto.subtle.digest("SHA-256", rawPublicKeyBuffer);
|
|
144
|
+
const keyId = base64urlEncode(new Uint8Array(keyIdBuffer)).substring(0, 16);
|
|
145
|
+
const keyInfo = {
|
|
146
|
+
keyId,
|
|
147
|
+
publicKey: publicKeyBase58
|
|
148
|
+
};
|
|
149
|
+
await this.storeKeyPair(this.cryptoKeyPair, keyInfo);
|
|
150
|
+
return keyInfo;
|
|
151
|
+
}
|
|
152
|
+
async storeKeyPair(keyPair, keyInfo) {
|
|
153
|
+
if (!this.db) {
|
|
154
|
+
throw new Error("Database not initialized");
|
|
155
|
+
}
|
|
156
|
+
return new Promise((resolve, reject) => {
|
|
157
|
+
const transaction = this.db.transaction([this.storeName], "readwrite");
|
|
158
|
+
const store = transaction.objectStore(this.storeName);
|
|
159
|
+
const keyPairRequest = store.put(keyPair, `${this.keyName}-keypair`);
|
|
160
|
+
const keyInfoRequest = store.put(keyInfo, `${this.keyName}-info`);
|
|
161
|
+
let completed = 0;
|
|
162
|
+
const total = 2;
|
|
163
|
+
const checkComplete = () => {
|
|
164
|
+
completed++;
|
|
165
|
+
if (completed === total) {
|
|
166
|
+
resolve();
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
keyPairRequest.onsuccess = checkComplete;
|
|
170
|
+
keyInfoRequest.onsuccess = checkComplete;
|
|
171
|
+
keyPairRequest.onerror = () => reject(keyPairRequest.error);
|
|
172
|
+
keyInfoRequest.onerror = () => reject(keyInfoRequest.error);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
async loadKeyPair() {
|
|
176
|
+
if (!this.db) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
return new Promise((resolve, reject) => {
|
|
180
|
+
const transaction = this.db.transaction([this.storeName], "readonly");
|
|
181
|
+
const store = transaction.objectStore(this.storeName);
|
|
182
|
+
const request = store.get(`${this.keyName}-keypair`);
|
|
183
|
+
request.onsuccess = () => {
|
|
184
|
+
this.cryptoKeyPair = request.result || null;
|
|
185
|
+
resolve();
|
|
186
|
+
};
|
|
187
|
+
request.onerror = () => reject(request.error);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
async getStoredKeyInfo() {
|
|
191
|
+
if (!this.db) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
return new Promise((resolve, reject) => {
|
|
195
|
+
const transaction = this.db.transaction([this.storeName], "readonly");
|
|
196
|
+
const store = transaction.objectStore(this.storeName);
|
|
197
|
+
const request = store.get(`${this.keyName}-info`);
|
|
198
|
+
request.onsuccess = () => resolve(request.result || null);
|
|
199
|
+
request.onerror = () => reject(request.error);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
export {
|
|
204
|
+
IndexedDbStamper
|
|
205
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@phantom/indexed-db-stamper",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "IndexedDB stamper for Phantom Wallet SDK with non-extractable key storage",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.mjs",
|
|
11
|
+
"require": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"?pack-release": "When https://github.com/changesets/changesets/issues/432 has a solution we can remove this trick",
|
|
17
|
+
"pack-release": "rimraf ./_release && yarn pack && mkdir ./_release && tar zxvf ./package.tgz --directory ./_release && rm ./package.tgz",
|
|
18
|
+
"build": "rimraf ./dist && tsup",
|
|
19
|
+
"dev": "tsc --watch",
|
|
20
|
+
"clean": "rm -rf dist",
|
|
21
|
+
"test": "jest",
|
|
22
|
+
"test:watch": "jest --watch",
|
|
23
|
+
"lint": "tsc --noEmit && eslint --cache . --ext .ts,.tsx",
|
|
24
|
+
"check-types": "tsc --noEmit",
|
|
25
|
+
"prettier": "prettier --write \"src/**/*.{ts,tsx}\""
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/bs58": "^5.0.0",
|
|
29
|
+
"@types/jest": "^29.5.12",
|
|
30
|
+
"@types/node": "^20.11.0",
|
|
31
|
+
"eslint": "8.53.0",
|
|
32
|
+
"fake-indexeddb": "^6.0.0",
|
|
33
|
+
"jest": "^29.7.0",
|
|
34
|
+
"jest-environment-jsdom": "^29.7.0",
|
|
35
|
+
"prettier": "^3.5.2",
|
|
36
|
+
"rimraf": "^6.0.1",
|
|
37
|
+
"ts-jest": "^29.1.2",
|
|
38
|
+
"tsup": "^6.7.0",
|
|
39
|
+
"typescript": "^5.0.4"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@phantom/base64url": "^0.1.0",
|
|
43
|
+
"@phantom/crypto": "^0.1.1",
|
|
44
|
+
"@phantom/embedded-provider-core": "^0.1.2",
|
|
45
|
+
"@phantom/sdk-types": "^0.1.1",
|
|
46
|
+
"bs58": "^6.0.0",
|
|
47
|
+
"buffer": "^6.0.3"
|
|
48
|
+
},
|
|
49
|
+
"files": [
|
|
50
|
+
"dist"
|
|
51
|
+
],
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"directory": "_release/package"
|
|
54
|
+
}
|
|
55
|
+
}
|