Skip to content

Commit eb819b2

Browse files
committed
WIP
1 parent 047987b commit eb819b2

2 files changed

Lines changed: 392 additions & 25 deletions

File tree

test-bootstrapping.ts

Lines changed: 342 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as jest from 'jest';
22
import * as jose from 'jose';
3-
import { hkdf, webcrypto } from 'crypto';
3+
import { hkdf, KeyObject, webcrypto } from 'crypto';
44
import * as bip39 from '@scure/bip39';
55
import { wordlist } from '@scure/bip39/wordlists/english';
66
import * as utils from '@noble/hashes/utils';
@@ -342,45 +342,362 @@ async function main () {
342342
// Let's generate a random DEK first
343343
// It will be 256 bits, as we will be using AES256GCM
344344
// Which is a 32 byte key
345+
345346
const dataEncryptionKey = getRandomBytesSync(32);
346347

348+
// DEK JWK
349+
const dekJSON = {
350+
alg: "A256GCM",
351+
kty: "oct",
352+
k: base64.base64url.baseEncode(dataEncryptionKey),
353+
ext: true,
354+
key_ops: ["encrypt", "decrypt"],
355+
};
356+
357+
console.log('IMPORT DEK');
358+
359+
const dekKey = await jose.importJWK(dekJSON, dekJSON.alg, false) as Uint8Array;
360+
361+
console.log(dekKey);
362+
363+
// KeyLike is KeyObject in nodejs or CryptoKey in browsers
364+
// There are improved security features when using these objects instead of Buffer
365+
// They can be passed to other threads using `postMessage`
366+
// the object is cloned...
367+
368+
// You can do KeyObject.from(CryptoKey)
369+
370+
// console.log(dekKey.type);
371+
// console.log(dekKey.symmetricKeySize);
372+
// console.log(dekKey.export({ format: 'jwk' })); // lol look at this
373+
374+
// dekKey.equals (compares another key object)
375+
// dekKey.symmetricKeySize
376+
// dekKey.type - public, private, secret
377+
378+
// Note that KeyObject is node specific
379+
// CryptoKey however is more general...
380+
// maybe we should use that?
381+
// dekKey.asymmetricKeyType
382+
// You just have to use `importKey`
383+
// but this is not that important
384+
// Ok we have the dekKey
385+
// It's time to encrypt this...
386+
// And use this for encryption...
387+
// If we wanted to use it for encryption, let's see how we would do this?
388+
389+
// iv would be random
390+
// createCipher
391+
392+
// But because JOSE uses platform native
393+
// and we want to use webcrypto API to avoid platform native
394+
// Here we would need to import the key
395+
// We can use webcrypto's importation of the key
396+
// OR we can use directly from the buffer
397+
// But this reads the JSON as well and extracts it
398+
399+
const dekCryptoKey = await webcrypto.subtle.importKey(
400+
'jwk',
401+
dekJSON,
402+
'AES-GCM',
403+
true,
404+
['encrypt', 'decrypt']
405+
);
406+
407+
console.log(dekCryptoKey);
408+
409+
const iv = getRandomBytesSync(16);
410+
411+
// This gives us a way to encrypt and decrypt now
412+
const cipherText = await webcrypto.subtle.encrypt(
413+
{
414+
name: 'AES-GCM',
415+
iv,
416+
tagLength: 128,
417+
},
418+
dekCryptoKey,
419+
Buffer.from('hello world')
420+
);
421+
422+
console.log('CIPHERTEXT', cipherText);
423+
424+
// The IV and the tag length must be shared
425+
// The ersulting data must be combined
426+
// [iv, authTag, cipherText]
427+
// however the authTag is already embedded in the cipherText
428+
429+
// This bundles it together
430+
// we can also just use this within the system
431+
// but I think the nodejs Buffer API is still better
432+
// we just need the feross API... etc
433+
434+
const combinedText = new Uint8Array(iv.length + cipherText.byteLength);
435+
const cipherArray = new Uint8Array(cipherText, 0, cipherText.byteLength);
436+
combinedText.set(iv);
437+
combinedText.set(cipherArray, iv.length);
438+
439+
console.log('COMBINED', combinedText);
440+
441+
// extracting it out of the combined text
442+
const iv_ = combinedText.subarray(0, iv.length);
443+
// The auth tag size will be consistent
444+
445+
const plainText = await webcrypto.subtle.decrypt(
446+
{
447+
name: 'AES-GCM',
448+
iv: iv_,
449+
tagLength: 128
450+
},
451+
dekCryptoKey,
452+
cipherText
453+
);
454+
455+
console.log(Buffer.from(plainText).toString());
456+
457+
458+
459+
// ---
460+
461+
console.log('NOW WE HAVE THE DEK')
462+
console.log('WE ARE GOING TO ENCRYPT OUR JWK');
463+
// dekJSON will be the JWK
464+
// we will encrypt this like above
465+
// It's time to use dir or ECDH-ES or somethig else
466+
467+
// This dekJSON is a symmetric key
468+
// However we are going to do KEM
469+
// by encrypting the dekJSON JWK file data
470+
// with our ed25519 key
471+
// To do so, we will first
472+
// acquire the shared secret via DX
473+
// Then pass it to HKDF-Extract with a static salt (for domain separation)
474+
// Then pass it to HKDF-Expand - with static info to produce the key which is used
475+
// for direct encryption of the JWK here
476+
477+
// This is the DH KX, getting us the shared secret
478+
// this is the "z" value, it's a "shared secret" between me and me (in the future)
347479
const x25519sharedsecret = await nobleEd.getSharedSecret(
348480
rootKeyPair.privateKey,
349481
rootKeyPair.publicKey
350482
);
351483

352-
// This is 32 bytes
353-
console.log(x25519sharedsecret);
354-
355484
// Now we use hkdf-extract
356485

357-
// The salt also has to be auto generated...
358-
// but i really don't think we need to do this
359-
// It's not some password
360-
// It's the actual derived key from ed25519
361-
// so what's the point of the salt here?
362-
486+
// Produce a pseudo random key
487+
// this is deterministic
488+
// Because we are using the same shared secret above
489+
// we are going to do this ONCE without a salt
490+
// then produce multiple subkeys
363491
const PRK = nobleHkdf.extract(
364492
nobleSha512,
365493
x25519sharedsecret,
366-
Buffer.from('some password')
367494
);
368495

369496
// This is 64 bytes
370-
console.log(PRK);
371-
372-
// But maybe we should be using JWK here again
373-
// And use Key Wrapping but this time with ed25519
374-
// Well here is the problem
375-
// The DEK is randomly generated
376-
// Now we are trying to encrypt it
377-
// encrypting it rquires a key symmetric key too?
378-
// So encrypted JWK is...?
379-
// Cause the salt is needed And where we saving the salt
380-
// Or not do it aall?
381-
// Let's import it into JWK first
382-
383-
// I think we don't need the salt, simply because no precomputation attack is possible here
497+
// Whether it produces 64 bytes or 32 bytes dpends on the input hash
498+
499+
console.log('PRK from HKDF-extract', PRK);
500+
const PRK2 = nobleHkdf.extract(
501+
nobleSha512,
502+
x25519sharedsecret,
503+
Buffer.from('domain separated')
504+
);
505+
console.log('PRK from HKDF-extract', PRK2);
506+
507+
// The info is useful here...
508+
// For separating to different keys
509+
const dbKeyKW = nobleHkdf.expand(
510+
nobleSha512,
511+
PRK,
512+
Buffer.from('DB KEY key wrap/key encapsulation mechanism'),
513+
32
514+
);
515+
516+
// And this is 32 bytes
517+
console.log('DBKEYKW', dbKeyKW);
518+
519+
// Ok great now we have the CEK to be used
520+
// the question does JWA have this mechanism built in?
521+
// Rather than us defining it?
522+
// dir means direct encryption
523+
524+
// alg: dir
525+
526+
// The reason if we use AES KW
527+
// it means the CEK itself is encrypted...
528+
// The CEK encrypts the actual plaintext
529+
// But the CEK itself is encrypted with AESKW
530+
531+
const dekJWE = new jose.FlattenedEncrypt(
532+
Buffer.from(JSON.stringify(dekJSON), 'utf-8')
533+
);
534+
535+
// Ok so what this does, is that it auto generates a CEK
536+
// that CEK uses A256GCM to encrypt the actual DEK above
537+
// But then takes a symmetric A256KW to encrypt the CEK
538+
// This is where it doesn't make sense to do this
539+
540+
// We cannot use the Ed25519 private key
541+
// We cannot use the shared secret
542+
// We cannot use the PRK
543+
// We can use the OKM from HKDF to do this (since it can be used as a symmetric key)
544+
// But here, it's a bit of a waste
545+
// Cause it's like
546+
// We are using a symmetric key to encrypt a symmetric key to encrypt a symmetric key
547+
// OKM -> CEK -> DEK
548+
// sym sym sym
549+
// It's just a bit dumb
550+
551+
dekJWE.setProtectedHeader({
552+
alg: 'A256KW',
553+
enc: 'A256GCM',
554+
cty: 'jwk+JSON'
555+
});
556+
557+
const inputE = getRandomBytesSync(32);
558+
559+
// You have to have a 256 bit key here to do the job
560+
const encryptedDEKJWK = await dekJWE.encrypt(
561+
inputE
562+
);
563+
564+
// I wonder how this actually works
565+
console.log(encryptedDEKJWK);
566+
567+
console.log(
568+
'WHAT IS THIS',
569+
await jose.flattenedDecrypt(
570+
encryptedDEKJWK,
571+
inputE
572+
)
573+
);
574+
575+
// Let's try something different
576+
577+
const dekJWEAgain = new jose.FlattenedEncrypt(
578+
Buffer.from(JSON.stringify(dekJSON), 'utf-8')
579+
);
580+
581+
dekJWEAgain.setProtectedHeader({
582+
alg: 'dir',
583+
enc: 'A256GCM',
584+
cty: 'jwk+JSON'
585+
});
586+
587+
const encryptedDEKJWKAgain = await dekJWEAgain.encrypt(dbKeyKW);
588+
589+
// Notice there's no `encrypted_key` property, the CEK is therefore empty
590+
console.log(encryptedDEKJWKAgain);
591+
592+
console.log(jose.decodeProtectedHeader(encryptedDEKJWKAgain));
593+
594+
const decryptedAgain = await jose.flattenedDecrypt(encryptedDEKJWKAgain, dbKeyKW);
595+
596+
console.log(decryptedAgain.plaintext.toString());
597+
598+
// This is why there was meant to be a keyring database
599+
// But this database is just disk based, no db is involved at all
600+
// If the root key ever changes, you don't change the DEK
601+
// But you do need to decrypt the JWK and re-encrypt it
602+
603+
// With AES KW, you can do the same... but only teh CEK
604+
// but the CEK is just somewhat smaller... it's not ereally that different
605+
606+
// alg: ECDH-ES+A256KW - this technically what we are doing...
607+
// enc: A256GCM - to do the actual encryption
608+
// how do use this?
609+
// except it's using CONCAT KDF
610+
611+
// ECDH-ES is direct key agreement mode
612+
// but it uses Concat KDF, so I don't think they are using HKDF
613+
614+
// It seems there's an extra RFC at 8037 to allow the usage of ED25519...
615+
// but it has to use x25519... you have to convert it first
616+
// perhpas it sort of works
617+
// But it continues to use Concat-KDF
618+
// Actually let's see if this works atm
619+
620+
621+
const dekJWEWithEC = new jose.FlattenedEncrypt(
622+
Buffer.from(JSON.stringify(dekJSON), 'utf-8')
623+
);
624+
625+
dekJWEWithEC.setProtectedHeader({
626+
alg: 'ECDH-ES',
627+
enc: 'A256GCM',
628+
cty: 'jwk+JSON',
629+
});
630+
631+
// console.log(rootKeyPair);
632+
633+
// You get a public x25519, and nothing else
634+
const publicX25519 = nobleEd.curve25519.scalarMultBase(rootKeyPair.privateKey);
635+
console.log('original', rootKeyPair.privateKey);
636+
console.log('PUBLIC X25519', publicX25519);
637+
638+
const y = {
639+
alg: 'X25519',
640+
kty: 'OKP',
641+
crv: 'X25519',
642+
x: base64.base64url.baseEncode(publicX25519),
643+
ext: true,
644+
key_ops: ['encrypt']
645+
};
646+
647+
console.log('Y', y);
648+
649+
const x25519keylike = await jose.importJWK(y) as jose.KeyLike;
650+
651+
console.log(x25519keylike);
652+
653+
// dekJWEWithEC.setKeyManagementParameters({
654+
// epk: x25519keylike
655+
// });
656+
657+
console.log('BEFORE WTF');
658+
659+
// // Do we encrypt with the public key?
660+
// // Or enrypt with the private key?
661+
const result = await dekJWEWithEC.encrypt(x25519keylike);
662+
663+
console.log('WTF?', result);
664+
665+
console.log(jose.decodeProtectedHeader(result));
666+
667+
// I'm not sure if this makes sense
668+
// unless you derive the private key too
669+
670+
const z = {
671+
alg: 'X25519',
672+
kty: 'OKP',
673+
crv: 'X25519',
674+
x: base64.base64url.baseEncode(publicX25519),
675+
d: base64.base64url.baseEncode(rootKeyPair.privateKey),
676+
ext: true,
677+
key_ops: ['decrypt']
678+
};
679+
680+
console.log('Z', z);
681+
682+
const privatex25519 = await jose.importJWK(z) as jose.KeyLike;
683+
684+
console.log('PRIVATE X25519', privatex25519);
685+
686+
const omg = await jose.flattenedDecrypt(result, privatex25519);
687+
688+
689+
console.log('TH shared secret', base64.base64url.baseEncode(x25519sharedsecret));
690+
691+
console.log(omg.plaintext.toString());
692+
693+
694+
695+
696+
697+
698+
699+
// const jwe = new jose.FlattenedEncrypt(Buffer.from(privateKeyJSONstring, 'utf-8'));
700+
384701

385702
// 2 options
386703
// alg: dir - https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-algorithms-18#section-4.5 - directly using a symmetric shared secret using ECDH and HKDF?

0 commit comments

Comments
 (0)