|
1 | 1 | import * as jest from 'jest'; |
2 | 2 | import * as jose from 'jose'; |
3 | | -import { hkdf, webcrypto } from 'crypto'; |
| 3 | +import { hkdf, KeyObject, webcrypto } from 'crypto'; |
4 | 4 | import * as bip39 from '@scure/bip39'; |
5 | 5 | import { wordlist } from '@scure/bip39/wordlists/english'; |
6 | 6 | import * as utils from '@noble/hashes/utils'; |
@@ -342,45 +342,362 @@ async function main () { |
342 | 342 | // Let's generate a random DEK first |
343 | 343 | // It will be 256 bits, as we will be using AES256GCM |
344 | 344 | // Which is a 32 byte key |
| 345 | + |
345 | 346 | const dataEncryptionKey = getRandomBytesSync(32); |
346 | 347 |
|
| 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) |
347 | 479 | const x25519sharedsecret = await nobleEd.getSharedSecret( |
348 | 480 | rootKeyPair.privateKey, |
349 | 481 | rootKeyPair.publicKey |
350 | 482 | ); |
351 | 483 |
|
352 | | - // This is 32 bytes |
353 | | - console.log(x25519sharedsecret); |
354 | | - |
355 | 484 | // Now we use hkdf-extract |
356 | 485 |
|
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 |
363 | 491 | const PRK = nobleHkdf.extract( |
364 | 492 | nobleSha512, |
365 | 493 | x25519sharedsecret, |
366 | | - Buffer.from('some password') |
367 | 494 | ); |
368 | 495 |
|
369 | 496 | // 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 | + |
384 | 701 |
|
385 | 702 | // 2 options |
386 | 703 | // 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