Skip to content

Commit 25a4fbc

Browse files
committed
WIP
1 parent 5e3764b commit 25a4fbc

3 files changed

Lines changed: 236 additions & 12 deletions

File tree

package-lock.json

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

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
"@matrixai/workers": "^1.3.6",
9191
"@noble/ed25519": "^1.7.1",
9292
"@noble/hashes": "^1.1.2",
93+
"@scure/bip39": "^1.1.0",
9394
"ajv": "^7.0.4",
9495
"bip39": "^3.0.3",
9596
"canonicalize": "^1.0.5",

test-bootstrapping.ts

Lines changed: 194 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,26 @@
1+
import * as jest from 'jest';
2+
import * as jose from 'jose';
13
import { webcrypto } from 'crypto';
2-
import * as bip39 from 'bip39';
4+
import * as bip39 from '@scure/bip39';
5+
import { wordlist } from '@scure/bip39/wordlists/english';
6+
import * as utils from '@noble/hashes/utils';
37
import * as nobleEd from '@noble/ed25519';
8+
import * as base64 from 'multiformats/bases/base64';
49
import * as noblePbkdf2 from '@noble/hashes/pbkdf2';
5-
import * as nobleSha256 from '@noble/hashes/sha256';
10+
import { sha512 as nobleSha512 } from '@noble/hashes/sha512';
11+
12+
// @ts-ignore - this overrides the random source used by @noble and @scure libraries
13+
utils.randomBytes = (size: number = 32) => getRandomBytesSync(size);
14+
nobleEd.utils.randomBytes = (size: number = 32) => getRandomBytesSync(size);
15+
16+
// Note that NodeJS Buffer is also Uint8Array
17+
function getRandomBytesSync(size: number): Uint8Array {
18+
console.log('CUSTOM CALLED');
19+
const randomArray = webcrypto.getRandomValues(new Uint8Array(size));
20+
return randomArray;
21+
// return Buffer.from(randomArray, randomArray.byteOffset, randomArray.byteLength);
22+
}
623

7-
// webcrypto is used for symmetric encryption
824

925
/**
1026
* Opaque types are wrappers of existing types
@@ -15,29 +31,195 @@ declare const brand: unique symbol;
1531

1632
type RecoveryCode = Opaque<'RecoveryCode', string>;
1733

18-
function getRandomBytesSync(size: number): Buffer {
19-
const randomArray = webcrypto.getRandomValues(new Uint8Array(size));
20-
return Buffer.from(randomArray, randomArray.byteOffset, randomArray.byteLength);
21-
}
34+
35+
// webcrypto is used for symmetric encryption
2236

2337
function generateRecoveryCode(size: 12 | 24 = 24): RecoveryCode {
2438
if (size === 12) {
25-
return bip39.generateMnemonic(128, getRandomBytesSync) as RecoveryCode;
39+
return bip39.generateMnemonic(wordlist, 128) as RecoveryCode;
2640
} else if (size === 24) {
27-
return bip39.generateMnemonic(256, getRandomBytesSync) as RecoveryCode;
41+
return bip39.generateMnemonic(wordlist, 256) as RecoveryCode;
2842
}
2943
throw RangeError(size);
3044
}
3145

46+
async function generateDeterministicKeyPair(recoveryCode: string) {
47+
// This uses BIP39 standard, the result is 64 byte seed
48+
// This is deterministic, and does not use any random source
49+
const recoverySeed = await bip39.mnemonicToSeed(recoveryCode);
50+
// Slice it to 32 bytes, as ed25519 private key is only 32 bytes
51+
const privateKey = recoverySeed.slice(0, 32);
52+
const publicKey = await nobleEd.getPublicKey(privateKey);
53+
return {
54+
publicKey,
55+
privateKey
56+
};
57+
}
58+
3259
async function main () {
3360

3461
const recoveryCode = generateRecoveryCode(24);
3562

36-
console.log(recoveryCode);
63+
console.log('RECOVERY CODE', recoveryCode);
3764

38-
// console.log(nobleEd.utils.randomPrivateKey());
65+
const rootKeyPair = await generateDeterministicKeyPair(recoveryCode);
66+
67+
console.log('ROOT KEY PAIR', rootKeyPair);
68+
69+
// How do we turn it into a JWK?
70+
// unless you use webcrypto to do this
71+
// This is a bit weird
72+
73+
// webcrypto.subtle.importKey(
74+
// 'raw',
75+
// rootKeyPair.privateKey,
76+
// 'Ed25519',
77+
// true,
78+
// ['sign']
79+
// );
80+
81+
// JWK uses base64url encoding, not base64 encoding
82+
const d = base64.base64url.baseEncode(rootKeyPair.privateKey);
83+
const x = base64.base64url.baseEncode(rootKeyPair.publicKey);
84+
85+
// This will import into "opaque" keylike objects
86+
// These can be used by jose operations
87+
88+
// If you pass in `d` you must pass in `x`, this gives you a private key
89+
// If you pass only `x`, this gives you a public key JWK
90+
const privateKey = await jose.importJWK({
91+
alg: 'EdDSA',
92+
kty: 'OKP', // Octet key pair
93+
crv: 'Ed25519', // Curve
94+
d: d, // Private key
95+
x: x, // Public key
96+
ext: true, // Extractable (always true in nodejs)
97+
key_ops: ['sign', 'verify'], // Key operations
98+
}) as jose.KeyLike;
99+
100+
const publicKey = await jose.importJWK({
101+
alg: 'EdDSA',
102+
kty: 'OKP', // Octet key pair
103+
crv: 'Ed25519', // Curve
104+
x: x, // Public key
105+
ext: true, // Extractable (always true in nodejs)
106+
key_ops: ['verify'], // Key operations
107+
}) as jose.KeyLike;
108+
109+
// We can alo use x5u parameter
110+
// But it is a URI pointing to it
111+
// We can do this.. by providing a URI to a pk resource
112+
// Like pk:://<NodeId>/certificate
113+
// A URI resource
114+
// The key in the first certificate
115+
// must match the public key represented by other members of JWK
116+
// Also x5c parameter too
117+
118+
console.log('PRIVATE', privateKey);
119+
console.log('PUBLIC', publicKey);
120+
121+
// JOSE should also have overrides secrets...
122+
123+
// Private Key PEM
124+
console.log(await jose.exportPKCS8(privateKey));
125+
126+
// Public Key PEM
127+
console.log(await jose.exportSPKI(publicKey));
128+
129+
130+
// This does not "preserve" all the JWK information
131+
// I actually have constructed it already above
132+
console.log(await jose.exportJWK(privateKey));
133+
134+
// We shouldn't use this
135+
// we can use the above to maintain information about the JWK
136+
console.log(await jose.exportJWK(publicKey));
137+
138+
const testJWK = {
139+
"kty":"RSA",
140+
"kid":"juliet@capulet.lit",
141+
"use":"enc",
142+
"n":"t6Q8PWSi1dkJj9hTP8hNYFlvadM7DflW9mWepOJhJ66w7nyoK1gPNqFMSQRyO125Gp-TEkodhWr0iujjHVx7BcV0llS4w5ACGgPrcAd6ZcSR0-Iqom-QFcNP8Sjg086MwoqQU_LYywlAGZ21WSdS_PERyGFiNnj3QQlO8Yns5jCtLCRwLHL0Pb1fEv45AuRIuUfVcPySBWYnDyGxvjYGDSM-AqWS9zIQ2ZilgT-GqUmipg0XOC0Cc20rgLe2ymLHjpHciCKVAbY5-L32-lSeZO-Os6U15_aXrk9Gw8cPUaX1_I8sLGuSiVdt3C_Fn2PZ3Z8i744FPFGGcG1qs2Wz-Q",
143+
"e":"AQAB",
144+
"d":"GRtbIQmhOZtyszfgKdg4u_N-R_mZGU_9k7JQ_jn1DnfTuMdSNprTeaSTyWfSNkuaAwnOEbIQVy1IQbWVV25NY3ybc_IhUJtfri7bAXYEReWaCl3hdlPKXy9UvqPYGR0kIXTQRqns-dVJ7jahlI7LyckrpTmrM8dWBo4_PMaenNnPiQgO0xnuToxutRZJfJvG4Ox4ka3GORQd9CsCZ2vsUDmsXOfUENOyMqADC6p1M3h33tsurY15k9qMSpG9OX_IJAXmxzAh_tWiZOwk2K4yxH9tS3Lq1yX8C1EWmeRDkK2ahecG85-oLKQt5VEpWHKmjOi_gJSdSgqcN96X52esAQ",
145+
"p":"2rnSOV4hKSN8sS4CgcQHFbs08XboFDqKum3sc4h3GRxrTmQdl1ZK9uw-PIHfQP0FkxXVrx-WE-ZEbrqivH_2iCLUS7wAl6XvARt1KkIaUxPPSYB9yk31s0Q8UK96E3_OrADAYtAJs-M3JxCLfNgqh56HDnETTQhH3rCT5T3yJws",
146+
"q":"1u_RiFDP7LBYh3N4GXLT9OpSKYP0uQZyiaZwBtOCBNJgQxaj10RWjsZu0c6Iedis4S7B_coSKB0Kj9PaPaBzg-IySRvvcQuPamQu66riMhjVtG6TlV8CLCYKrYl52ziqK0E_ym2QnkwsUX7eYTB7LbAHRK9GqocDE5B0f808I4s",
147+
"dp":"KkMTWqBUefVwZ2_Dbj1pPQqyHSHjj90L5x_MOzqYAJMcLMZtbUtwKqvVDq3tbEo3ZIcohbDtt6SbfmWzggabpQxNxuBpoOOf_a_HgMXK_lhqigI4y_kqS1wY52IwjUn5rgRrJ-yYo1h41KR-vz2pYhEAeYrhttWtxVqLCRViD6c",
148+
"dq":"AvfS0-gRxvn0bwJoMSnFxYcK1WnuEjQFluMGfwGitQBWtfZ1Er7t1xDkbN9GQTB9yqpDoYaN06H7CFtrkxhJIBQaj6nkF5KKS3TQtQ5qCzkOkmxIe3KRbBymXxkb5qwUpX5ELD5xFc6FeiafWYY63TmmEAu_lRFCOJ3xDea-ots",
149+
"qi":"lSQi-w9CpyUReMErP1RsBLk7wNtOvs5EQpPqmuMvqW57NBUczScEoPwmUqqabu9V0-Py4dQ57_bapoKRu1R90bvuFnU63SHWEFglZQvJDMeAvmj4sm-Fp0oYu_neotgQ0hzbI5gry7ajdYy9-2lNx_76aBZoOUu9HCJ-UsfSOI8"
150+
};
39151

40-
// Need to seed a random source with this
152+
const testJWKS = JSON.stringify(testJWK);
153+
154+
console.log([...Buffer.from(testJWKS)]);
155+
156+
/*
157+
THIS IS THE JWE HEADER!!
158+
159+
{
160+
alg: "PBES2-HS256+A128KW", // algorithm used for encryption
161+
p2s: "...", // salt
162+
p2c: "...", // iteration count
163+
enc: "A1128CBC-HS256", // authenticated encryption
164+
cty: "jwk+json", // It is an application/jwk+json type
165+
}
166+
167+
The JWE protected header is then base64url(utf8(json stringify)) encoded
168+
169+
SOMESTRING...
170+
171+
CONTENT encryption key is generated... (or derived from password)
172+
It's 256 bits
173+
174+
The content encryption key is what is used to encrypt the data the JWK string.
175+
Wait a minute... what is this?
176+
Oh so this is the key
177+
178+
*/
179+
180+
181+
182+
183+
184+
185+
// To do this
186+
// we can use webcrypto
187+
// OR we can use jose
188+
189+
190+
// Ok we got the root key pair
191+
// Encode as JWK so it can be saved on disk
192+
// Encrypt the private key with a root password using a symmetric cipher
193+
// To do this, the root password must be hashed with a key derivation function
194+
// Which is then used to encrypt the private key JWK file
195+
// We can also use 2 files
196+
// Or 1 file being JWKS - a set of JWKs
197+
// The reason to use 2 files, is that the public key doesn't need encryption
198+
199+
200+
201+
// Now with the ed25519 master key
202+
// We should be able to "derive/generate" subkeys DEKs
203+
// These DEKs - the database one in particular should be used
204+
// to encrypt the database
205+
// The DEKs can be randomly generated, they do not need to be "connected"
206+
// to the Master Key
207+
// But the DEK when outside the DB, needs to be stored on disk
208+
// In particular the DEK used for the DB must be encrypted with the root key
209+
// To do this the root key can be directly used for encryption
210+
// Converted to X25519 to encrypt
211+
// Or use an HKDF over the ed25519 key to do so...
212+
213+
214+
215+
216+
// The ed25519 key is not intended for encryption
217+
// We can "derive" a x25519 key to do the encryption
218+
// This avoids using 2 keys
219+
// Some places argue to create a second key to do this
220+
221+
222+
// console.log(nobleEd.utils.randomPrivateKey());
41223

42224
}
43225

0 commit comments

Comments
 (0)