lockform 2.0.0 → 3.0.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 +83 -10
- package/derive-key.js +50 -0
- package/dist/crypto.d.ts +1 -0
- package/dist/crypto.js +5 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -1
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -14,21 +14,21 @@ npm install lockform
|
|
|
14
14
|
- **Signature verification**: Verify webhook authenticity using HMAC-SHA256 signatures
|
|
15
15
|
- **Field mapping**: Automatically map field IDs to human-readable CSV names
|
|
16
16
|
- **X25519 encryption**: Modern, fast elliptic curve cryptography
|
|
17
|
-
- **BIP39 support**: Works with 15-word
|
|
17
|
+
- **BIP39 support**: Works with 15-word passphrases or base64 private keys
|
|
18
18
|
- **TypeScript support**: Full type definitions included
|
|
19
19
|
|
|
20
20
|
## Quick Start
|
|
21
21
|
|
|
22
22
|
### Decrypting Webhook Data
|
|
23
23
|
|
|
24
|
-
You can decrypt webhooks using either your 15-word
|
|
24
|
+
You can decrypt webhooks using either your 15-word passphrase or a base64-encoded private key:
|
|
25
25
|
|
|
26
|
-
**Using
|
|
26
|
+
**Using passphrase:**
|
|
27
27
|
|
|
28
28
|
```typescript
|
|
29
29
|
import { decryptWebhookData } from 'lockform'
|
|
30
30
|
|
|
31
|
-
const mnemonic = 'your fifteen word
|
|
31
|
+
const mnemonic = 'your fifteen word passphrase goes here and must be exactly fifteen words'
|
|
32
32
|
|
|
33
33
|
app.post('/webhook', async (req, res) => {
|
|
34
34
|
const payload = req.body
|
|
@@ -94,13 +94,38 @@ app.post('/webhook', async (req, res) => {
|
|
|
94
94
|
|
|
95
95
|
## API Reference
|
|
96
96
|
|
|
97
|
+
### `derivePrivateKey(mnemonic)`
|
|
98
|
+
|
|
99
|
+
Derives a base64-encoded X25519 private key from a 15-word BIP39 passphrase. Useful for generating keys to use in edge functions.
|
|
100
|
+
|
|
101
|
+
**Parameters:**
|
|
102
|
+
- `mnemonic` (string): Your 15-word BIP39 passphrase
|
|
103
|
+
|
|
104
|
+
**Returns:** `string` - Base64-encoded X25519 private key
|
|
105
|
+
|
|
106
|
+
**Example:**
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import { derivePrivateKey } from 'lockform'
|
|
110
|
+
|
|
111
|
+
const mnemonic = 'your fifteen word passphrase goes here exactly fifteen words'
|
|
112
|
+
const privateKeyBase64 = derivePrivateKey(mnemonic)
|
|
113
|
+
|
|
114
|
+
console.log(privateKeyBase64)
|
|
115
|
+
// Output: "a1b2c3d4e5f6..." (base64 string)
|
|
116
|
+
|
|
117
|
+
// Store this in your edge function environment variable
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Use case:** Run this once locally to derive your private key, then store the base64 output as an environment variable for edge functions (to avoid PBKDF2 timeout).
|
|
121
|
+
|
|
97
122
|
### `decryptWebhookData(options)`
|
|
98
123
|
|
|
99
124
|
Decrypts an encrypted webhook payload from Lockform using X25519 + AES-256-GCM.
|
|
100
125
|
|
|
101
126
|
**Parameters:**
|
|
102
127
|
- `options.payload` (WebhookPayload): The webhook payload received from Lockform
|
|
103
|
-
- `options.passphrase` (string): Your 15-word BIP39
|
|
128
|
+
- `options.passphrase` (string): Your 15-word BIP39 passphrase (or optionally, base64-encoded X25519 private key)
|
|
104
129
|
|
|
105
130
|
**Returns:** `Promise<DecryptedSubmission>`
|
|
106
131
|
|
|
@@ -247,6 +272,8 @@ app.listen(3000, () => {
|
|
|
247
272
|
|
|
248
273
|
### Deno Edge Function
|
|
249
274
|
|
|
275
|
+
**Important:** Edge functions have strict CPU time limits. Use a base64-encoded private key instead of the 15-word passphrase to avoid CPU timeout errors. See the performance note below.
|
|
276
|
+
|
|
250
277
|
```typescript
|
|
251
278
|
import { decryptWebhookData, verifyWebhookSignature, type WebhookPayload } from 'lockform'
|
|
252
279
|
|
|
@@ -263,7 +290,7 @@ Deno.serve(async (req) => {
|
|
|
263
290
|
|
|
264
291
|
if (!recoveryPhrase) {
|
|
265
292
|
return new Response(
|
|
266
|
-
JSON.stringify({ error: '
|
|
293
|
+
JSON.stringify({ error: 'Passphrase not configured' }),
|
|
267
294
|
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
|
268
295
|
)
|
|
269
296
|
}
|
|
@@ -320,8 +347,8 @@ Lockform uses modern, audited cryptography for maximum security:
|
|
|
320
347
|
## Security Best Practices
|
|
321
348
|
|
|
322
349
|
1. **Always verify signatures**: Use `verifyWebhookSignature` to ensure webhooks are genuinely from Lockform
|
|
323
|
-
2. **Protect your
|
|
324
|
-
3. **Never share your
|
|
350
|
+
2. **Protect your passphrase**: Store your 15-word passphrase in environment variables, never commit it to version control
|
|
351
|
+
3. **Never share your passphrase**: Anyone with your 15-word passphrase can decrypt all submissions
|
|
325
352
|
4. **Use HTTPS**: Always use HTTPS endpoints for webhooks in production
|
|
326
353
|
5. **Validate data**: Always validate the decrypted data before processing it
|
|
327
354
|
6. **Implement idempotency**: Use the `submission_id` to prevent duplicate processing
|
|
@@ -332,14 +359,60 @@ Lockform uses modern, audited cryptography for maximum security:
|
|
|
332
359
|
If you're migrating from the RSA-based v1.x version:
|
|
333
360
|
|
|
334
361
|
1. **Update your package**: `npm install lockform@latest`
|
|
335
|
-
2. **Update your credentials**: Use your 15-word
|
|
336
|
-
3. **Update your code**: Pass your
|
|
362
|
+
2. **Update your credentials**: Use your 15-word passphrase instead of PEM-formatted RSA keys
|
|
363
|
+
3. **Update your code**: Pass your passphrase as the `passphrase` parameter (renamed from `privateKey`)
|
|
337
364
|
|
|
338
365
|
The webhook payload structure has changed:
|
|
339
366
|
- `wrapped_key` → `ephemeral_public_key`
|
|
340
367
|
- Added: `salt`, `encryption_timestamp`
|
|
341
368
|
- `algorithm` changed from `RSA-OAEP-4096+AES-256-GCM` to `X25519+AES-256-GCM`
|
|
342
369
|
|
|
370
|
+
## Performance Considerations
|
|
371
|
+
|
|
372
|
+
### Edge Functions (Deno, Cloudflare Workers, etc.)
|
|
373
|
+
|
|
374
|
+
Edge functions have strict CPU time limits (typically 50-100ms). The PBKDF2 key derivation with 600,000 iterations can take several seconds and will cause timeout errors.
|
|
375
|
+
|
|
376
|
+
**Solution:** Use a base64-encoded private key instead of the passphrase.
|
|
377
|
+
|
|
378
|
+
**Option 1: Use the CLI tool (easiest)**
|
|
379
|
+
|
|
380
|
+
```bash
|
|
381
|
+
npx lockform-derive-key
|
|
382
|
+
# Or if installed: npm run derive-key
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
This will prompt you for your passphrase and output the base64 private key.
|
|
386
|
+
|
|
387
|
+
**Option 2: Use the API programmatically**
|
|
388
|
+
|
|
389
|
+
```javascript
|
|
390
|
+
import { derivePrivateKey } from 'lockform'
|
|
391
|
+
|
|
392
|
+
const mnemonic = 'your fifteen word passphrase here exactly fifteen words'
|
|
393
|
+
const privateKeyBase64 = derivePrivateKey(mnemonic)
|
|
394
|
+
console.log(privateKeyBase64) // Store this in your edge function environment
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
Then use the base64 key in your edge function:
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
const recoveryPhrase = Deno.env.get('LOCKFORM_RECOVERY_PHRASE') // Now contains base64 key
|
|
401
|
+
|
|
402
|
+
const result = await decryptWebhookData({
|
|
403
|
+
payload,
|
|
404
|
+
passphrase: recoveryPhrase, // Automatically detects base64 format (no PBKDF2)
|
|
405
|
+
})
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
The library automatically detects the format:
|
|
409
|
+
- Contains spaces → 15-word mnemonic (slow, runs PBKDF2)
|
|
410
|
+
- No spaces → Base64 private key (fast, skips PBKDF2)
|
|
411
|
+
|
|
412
|
+
### Node.js / Long-running servers
|
|
413
|
+
|
|
414
|
+
You can use either format. The 15-word passphrase works fine in environments without strict CPU time limits.
|
|
415
|
+
|
|
343
416
|
## Requirements
|
|
344
417
|
|
|
345
418
|
- Node.js 18.0.0 or higher
|
package/derive-key.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { derivePrivateKey } from './dist/index.js'
|
|
3
|
+
import { createInterface } from 'readline'
|
|
4
|
+
|
|
5
|
+
const rl = createInterface({
|
|
6
|
+
input: process.stdin,
|
|
7
|
+
output: process.stdout
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
console.log('='.repeat(70))
|
|
11
|
+
console.log('Lockform Passphrase Key Derivation Tool')
|
|
12
|
+
console.log('='.repeat(70))
|
|
13
|
+
console.log()
|
|
14
|
+
console.log('This tool derives a base64-encoded X25519 key from your')
|
|
15
|
+
console.log('15-word BIP39 passphrase for use in edge functions.')
|
|
16
|
+
console.log()
|
|
17
|
+
|
|
18
|
+
rl.question('Enter your 15-word passphrase: ', (mnemonic) => {
|
|
19
|
+
try {
|
|
20
|
+
const words = mnemonic.trim().split(/\s+/)
|
|
21
|
+
|
|
22
|
+
if (words.length !== 15) {
|
|
23
|
+
console.error('\n❌ Error: Passphrase must be exactly 15 words')
|
|
24
|
+
console.error(` You entered ${words.length} words`)
|
|
25
|
+
process.exit(1)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
console.log('\n⏳ Deriving encryption key (this may take a few seconds)...\n')
|
|
29
|
+
|
|
30
|
+
const privateKeyBase64 = derivePrivateKey(mnemonic.trim())
|
|
31
|
+
|
|
32
|
+
console.log('✅ Success! Your base64-encoded encryption key:\n')
|
|
33
|
+
console.log('─'.repeat(70))
|
|
34
|
+
console.log(privateKeyBase64)
|
|
35
|
+
console.log('─'.repeat(70))
|
|
36
|
+
console.log()
|
|
37
|
+
console.log('Add this to your edge function environment variables:')
|
|
38
|
+
console.log()
|
|
39
|
+
console.log(`LOCKFORM_DERIVED_KEY="${privateKeyBase64}"`)
|
|
40
|
+
console.log()
|
|
41
|
+
console.log('⚠️ Keep this secret! Anyone with this key can decrypt all submissions.')
|
|
42
|
+
console.log()
|
|
43
|
+
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error('\n❌ Error:', error.message)
|
|
46
|
+
process.exit(1)
|
|
47
|
+
} finally {
|
|
48
|
+
rl.close()
|
|
49
|
+
}
|
|
50
|
+
})
|
package/dist/crypto.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export declare function base64ToArrayBuffer(base64: string): Promise<Uint8Array>
|
|
|
2
2
|
export declare function arrayBufferToString(buffer: ArrayBuffer | Uint8Array): string;
|
|
3
3
|
export declare function importPrivateKeyFromMnemonic(mnemonic: string): Uint8Array;
|
|
4
4
|
export declare function importPrivateKeyFromBase64(base64: string): Uint8Array;
|
|
5
|
+
export declare function derivePrivateKey(mnemonic: string): string;
|
|
5
6
|
export declare function decryptSubmission(ciphertext: string, iv: string, salt: string, ephemeralPublicKey: string, privateKey: Uint8Array, metadata?: {
|
|
6
7
|
formId?: string;
|
|
7
8
|
encryptionTimestamp?: number;
|
package/dist/crypto.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.base64ToArrayBuffer = base64ToArrayBuffer;
|
|
|
4
4
|
exports.arrayBufferToString = arrayBufferToString;
|
|
5
5
|
exports.importPrivateKeyFromMnemonic = importPrivateKeyFromMnemonic;
|
|
6
6
|
exports.importPrivateKeyFromBase64 = importPrivateKeyFromBase64;
|
|
7
|
+
exports.derivePrivateKey = derivePrivateKey;
|
|
7
8
|
exports.decryptSubmission = decryptSubmission;
|
|
8
9
|
exports.createHmacSignature = createHmacSignature;
|
|
9
10
|
const node_crypto_1 = require("node:crypto");
|
|
@@ -36,6 +37,10 @@ function importPrivateKeyFromMnemonic(mnemonic) {
|
|
|
36
37
|
function importPrivateKeyFromBase64(base64) {
|
|
37
38
|
return Buffer.from(base64, 'base64');
|
|
38
39
|
}
|
|
40
|
+
function derivePrivateKey(mnemonic) {
|
|
41
|
+
const privateKeyBytes = importPrivateKeyFromMnemonic(mnemonic);
|
|
42
|
+
return Buffer.from(privateKeyBytes).toString('base64');
|
|
43
|
+
}
|
|
39
44
|
function deriveSharedSecret(privateKey, publicKey) {
|
|
40
45
|
return ed25519_1.x25519.getSharedSecret(privateKey, publicKey);
|
|
41
46
|
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.verifyWebhookSignature = exports.decryptWebhookData = void 0;
|
|
3
|
+
exports.derivePrivateKey = exports.verifyWebhookSignature = exports.decryptWebhookData = void 0;
|
|
4
4
|
var decrypt_1 = require("./decrypt");
|
|
5
5
|
Object.defineProperty(exports, "decryptWebhookData", { enumerable: true, get: function () { return decrypt_1.decryptWebhookData; } });
|
|
6
6
|
var verify_1 = require("./verify");
|
|
7
7
|
Object.defineProperty(exports, "verifyWebhookSignature", { enumerable: true, get: function () { return verify_1.verifyWebhookSignature; } });
|
|
8
|
+
var crypto_1 = require("./crypto");
|
|
9
|
+
Object.defineProperty(exports, "derivePrivateKey", { enumerable: true, get: function () { return crypto_1.derivePrivateKey; } });
|
package/package.json
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lockform",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "Official SDK for processing Lockform webhook submissions with end-to-end encryption using X25519 + AES-256-GCM",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"files": [
|
|
8
|
-
"dist"
|
|
8
|
+
"dist",
|
|
9
|
+
"derive-key.js"
|
|
9
10
|
],
|
|
10
11
|
"scripts": {
|
|
11
12
|
"build": "tsc",
|
|
12
|
-
"prepublishOnly": "npm run build"
|
|
13
|
+
"prepublishOnly": "npm run build",
|
|
14
|
+
"derive-key": "node derive-key.js"
|
|
15
|
+
},
|
|
16
|
+
"bin": {
|
|
17
|
+
"lockform-derive-key": "./derive-key.js"
|
|
13
18
|
},
|
|
14
19
|
"keywords": [
|
|
15
20
|
"lockform",
|