1+ import * as jest from 'jest' ;
2+ import * as jose from 'jose' ;
13import { 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' ;
37import * as nobleEd from '@noble/ed25519' ;
8+ import * as base64 from 'multiformats/bases/base64' ;
49import * 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
1632type 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
2337function 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+
3259async 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