h3 1.2.0 → 1.2.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/dist/index.cjs +294 -5
- package/dist/index.d.ts +47 -1
- package/dist/index.mjs +290 -1
- package/package.json +1 -2
package/dist/index.cjs
CHANGED
|
@@ -4,7 +4,6 @@ const ufo = require('ufo');
|
|
|
4
4
|
const radix3 = require('radix3');
|
|
5
5
|
const destr = require('destr');
|
|
6
6
|
const cookieEs = require('cookie-es');
|
|
7
|
-
const ironWebcrypto = require('iron-webcrypto');
|
|
8
7
|
const crypto = require('uncrypto');
|
|
9
8
|
|
|
10
9
|
function useBase(base, handler) {
|
|
@@ -590,6 +589,296 @@ ${header}: ${value}`;
|
|
|
590
589
|
}
|
|
591
590
|
}
|
|
592
591
|
|
|
592
|
+
const base64urlEncode = (value) => (Buffer.isBuffer(value) ? value : Buffer.from(value)).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
593
|
+
const defaults = {
|
|
594
|
+
encryption: {
|
|
595
|
+
saltBits: 256,
|
|
596
|
+
algorithm: "aes-256-cbc",
|
|
597
|
+
iterations: 1,
|
|
598
|
+
minPasswordlength: 32
|
|
599
|
+
},
|
|
600
|
+
integrity: {
|
|
601
|
+
saltBits: 256,
|
|
602
|
+
algorithm: "sha256",
|
|
603
|
+
iterations: 1,
|
|
604
|
+
minPasswordlength: 32
|
|
605
|
+
},
|
|
606
|
+
ttl: 0,
|
|
607
|
+
timestampSkewSec: 60,
|
|
608
|
+
localtimeOffsetMsec: 0
|
|
609
|
+
};
|
|
610
|
+
const clone = (options) => ({
|
|
611
|
+
...options,
|
|
612
|
+
encryption: { ...options.encryption },
|
|
613
|
+
integrity: { ...options.integrity }
|
|
614
|
+
});
|
|
615
|
+
const algorithms = {
|
|
616
|
+
"aes-128-ctr": { keyBits: 128, ivBits: 128, name: "AES-CTR" },
|
|
617
|
+
"aes-256-cbc": { keyBits: 256, ivBits: 128, name: "AES-CBC" },
|
|
618
|
+
sha256: { keyBits: 256, name: "SHA-256" }
|
|
619
|
+
};
|
|
620
|
+
const macFormatVersion = "2";
|
|
621
|
+
const macPrefix = `Fe26.${macFormatVersion}`;
|
|
622
|
+
const randomBytes = (_crypto, size) => {
|
|
623
|
+
const bytes = Buffer.allocUnsafe(size);
|
|
624
|
+
_crypto.getRandomValues(bytes);
|
|
625
|
+
return bytes;
|
|
626
|
+
};
|
|
627
|
+
const randomBits = (_crypto, bits) => {
|
|
628
|
+
if (bits < 1) {
|
|
629
|
+
throw new Error("Invalid random bits count");
|
|
630
|
+
}
|
|
631
|
+
const bytes = Math.ceil(bits / 8);
|
|
632
|
+
return randomBytes(_crypto, bytes);
|
|
633
|
+
};
|
|
634
|
+
const pbkdf2 = async (_crypto, password, salt, iterations, keyLength, hash) => {
|
|
635
|
+
const textEncoder = new TextEncoder();
|
|
636
|
+
const passwordBuffer = textEncoder.encode(password);
|
|
637
|
+
const importedKey = await _crypto.subtle.importKey(
|
|
638
|
+
"raw",
|
|
639
|
+
passwordBuffer,
|
|
640
|
+
"PBKDF2",
|
|
641
|
+
false,
|
|
642
|
+
["deriveBits"]
|
|
643
|
+
);
|
|
644
|
+
const saltBuffer = textEncoder.encode(salt);
|
|
645
|
+
const params = { name: "PBKDF2", hash, salt: saltBuffer, iterations };
|
|
646
|
+
const derivation = await _crypto.subtle.deriveBits(
|
|
647
|
+
params,
|
|
648
|
+
importedKey,
|
|
649
|
+
keyLength * 8
|
|
650
|
+
);
|
|
651
|
+
return Buffer.from(derivation);
|
|
652
|
+
};
|
|
653
|
+
const generateKey = async (_crypto, password, options) => {
|
|
654
|
+
if (password == null || password.length === 0) {
|
|
655
|
+
throw new Error("Empty password");
|
|
656
|
+
}
|
|
657
|
+
if (options == null || typeof options !== "object") {
|
|
658
|
+
throw new Error("Bad options");
|
|
659
|
+
}
|
|
660
|
+
if (!(options.algorithm in algorithms)) {
|
|
661
|
+
throw new Error(`Unknown algorithm: ${options.algorithm}`);
|
|
662
|
+
}
|
|
663
|
+
const algorithm = algorithms[options.algorithm];
|
|
664
|
+
const result = {};
|
|
665
|
+
const hmac = options.hmac ?? false;
|
|
666
|
+
const id = hmac ? { name: "HMAC", hash: algorithm.name } : { name: algorithm.name };
|
|
667
|
+
const usage = hmac ? ["sign", "verify"] : ["encrypt", "decrypt"];
|
|
668
|
+
if (typeof password === "string") {
|
|
669
|
+
if (password.length < options.minPasswordlength) {
|
|
670
|
+
throw new Error(
|
|
671
|
+
`Password string too short (min ${options.minPasswordlength} characters required)`
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
let { salt = "" } = options;
|
|
675
|
+
if (!salt) {
|
|
676
|
+
const { saltBits = 0 } = options;
|
|
677
|
+
if (!saltBits) {
|
|
678
|
+
throw new Error("Missing salt and saltBits options");
|
|
679
|
+
}
|
|
680
|
+
const randomSalt = randomBits(_crypto, saltBits);
|
|
681
|
+
salt = randomSalt.toString("hex");
|
|
682
|
+
}
|
|
683
|
+
const derivedKey = await pbkdf2(
|
|
684
|
+
_crypto,
|
|
685
|
+
password,
|
|
686
|
+
salt,
|
|
687
|
+
options.iterations,
|
|
688
|
+
algorithm.keyBits / 8,
|
|
689
|
+
"SHA-1"
|
|
690
|
+
);
|
|
691
|
+
const importedEncryptionKey = await _crypto.subtle.importKey(
|
|
692
|
+
"raw",
|
|
693
|
+
derivedKey,
|
|
694
|
+
id,
|
|
695
|
+
false,
|
|
696
|
+
usage
|
|
697
|
+
);
|
|
698
|
+
result.key = importedEncryptionKey;
|
|
699
|
+
result.salt = salt;
|
|
700
|
+
} else {
|
|
701
|
+
if (password.length < algorithm.keyBits / 8) {
|
|
702
|
+
throw new Error("Key buffer (password) too small");
|
|
703
|
+
}
|
|
704
|
+
result.key = await _crypto.subtle.importKey(
|
|
705
|
+
"raw",
|
|
706
|
+
password,
|
|
707
|
+
id,
|
|
708
|
+
false,
|
|
709
|
+
usage
|
|
710
|
+
);
|
|
711
|
+
result.salt = "";
|
|
712
|
+
}
|
|
713
|
+
if (options.iv) {
|
|
714
|
+
result.iv = options.iv;
|
|
715
|
+
} else if ("ivBits" in algorithm) {
|
|
716
|
+
result.iv = randomBits(_crypto, algorithm.ivBits);
|
|
717
|
+
}
|
|
718
|
+
return result;
|
|
719
|
+
};
|
|
720
|
+
const encrypt = async (_crypto, password, options, data) => {
|
|
721
|
+
const key = await generateKey(_crypto, password, options);
|
|
722
|
+
const textEncoder = new TextEncoder();
|
|
723
|
+
const textBuffer = textEncoder.encode(data);
|
|
724
|
+
const encrypted = await _crypto.subtle.encrypt(
|
|
725
|
+
{ name: algorithms[options.algorithm].name, iv: key.iv },
|
|
726
|
+
key.key,
|
|
727
|
+
textBuffer
|
|
728
|
+
);
|
|
729
|
+
return { encrypted: Buffer.from(encrypted), key };
|
|
730
|
+
};
|
|
731
|
+
const decrypt = async (_crypto, password, options, data) => {
|
|
732
|
+
const key = await generateKey(_crypto, password, options);
|
|
733
|
+
const decrypted = await _crypto.subtle.decrypt(
|
|
734
|
+
{ name: algorithms[options.algorithm].name, iv: key.iv },
|
|
735
|
+
key.key,
|
|
736
|
+
Buffer.isBuffer(data) ? data : Buffer.from(data)
|
|
737
|
+
);
|
|
738
|
+
const textDecoder = new TextDecoder();
|
|
739
|
+
return textDecoder.decode(decrypted);
|
|
740
|
+
};
|
|
741
|
+
const hmacWithPassword = async (_crypto, password, options, data) => {
|
|
742
|
+
const key = await generateKey(_crypto, password, { ...options, hmac: true });
|
|
743
|
+
const textEncoder = new TextEncoder();
|
|
744
|
+
const textBuffer = textEncoder.encode(data);
|
|
745
|
+
const signed = await _crypto.subtle.sign(
|
|
746
|
+
{ name: "HMAC" },
|
|
747
|
+
key.key,
|
|
748
|
+
textBuffer
|
|
749
|
+
);
|
|
750
|
+
const digest = base64urlEncode(Buffer.from(signed));
|
|
751
|
+
return { digest, salt: key.salt };
|
|
752
|
+
};
|
|
753
|
+
const normalizePassword = (password) => {
|
|
754
|
+
if (typeof password === "object" && !Buffer.isBuffer(password)) {
|
|
755
|
+
if ("secret" in password) {
|
|
756
|
+
return {
|
|
757
|
+
id: password.id,
|
|
758
|
+
encryption: password.secret,
|
|
759
|
+
integrity: password.secret
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
return {
|
|
763
|
+
id: password.id,
|
|
764
|
+
encryption: password.encryption,
|
|
765
|
+
integrity: password.integrity
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
return { encryption: password, integrity: password };
|
|
769
|
+
};
|
|
770
|
+
const seal = async (_crypto, object, password, options) => {
|
|
771
|
+
if (!password) {
|
|
772
|
+
throw new Error("Empty password");
|
|
773
|
+
}
|
|
774
|
+
const opts = clone(options);
|
|
775
|
+
const now = Date.now() + (opts.localtimeOffsetMsec || 0);
|
|
776
|
+
const objectString = JSON.stringify(object);
|
|
777
|
+
const pass = normalizePassword(password);
|
|
778
|
+
const { id = "" } = pass;
|
|
779
|
+
if (id && !/^\w+$/.test(id)) {
|
|
780
|
+
throw new Error("Invalid password id");
|
|
781
|
+
}
|
|
782
|
+
const { encrypted, key } = await encrypt(
|
|
783
|
+
_crypto,
|
|
784
|
+
pass.encryption,
|
|
785
|
+
opts.encryption,
|
|
786
|
+
objectString
|
|
787
|
+
);
|
|
788
|
+
const encryptedB64 = base64urlEncode(encrypted);
|
|
789
|
+
const iv = base64urlEncode(key.iv);
|
|
790
|
+
const expiration = opts.ttl ? now + opts.ttl : "";
|
|
791
|
+
const macBaseString = `${macPrefix}*${id}*${key.salt}*${iv}*${encryptedB64}*${expiration}`;
|
|
792
|
+
const mac = await hmacWithPassword(
|
|
793
|
+
_crypto,
|
|
794
|
+
pass.integrity,
|
|
795
|
+
opts.integrity,
|
|
796
|
+
macBaseString
|
|
797
|
+
);
|
|
798
|
+
const sealed = `${macBaseString}*${mac.salt}*${mac.digest}`;
|
|
799
|
+
return sealed;
|
|
800
|
+
};
|
|
801
|
+
const fixedTimeComparison = (a, b) => {
|
|
802
|
+
let mismatch = a.length === b.length ? 0 : 1;
|
|
803
|
+
if (mismatch) {
|
|
804
|
+
b = a;
|
|
805
|
+
}
|
|
806
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
807
|
+
mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
808
|
+
}
|
|
809
|
+
return mismatch === 0;
|
|
810
|
+
};
|
|
811
|
+
const unseal = async (_crypto, sealed, password, options) => {
|
|
812
|
+
if (!password) {
|
|
813
|
+
throw new Error("Empty password");
|
|
814
|
+
}
|
|
815
|
+
const opts = clone(options);
|
|
816
|
+
const now = Date.now() + (opts.localtimeOffsetMsec || 0);
|
|
817
|
+
const parts = sealed.split("*");
|
|
818
|
+
if (parts.length !== 8) {
|
|
819
|
+
throw new Error("Incorrect number of sealed components");
|
|
820
|
+
}
|
|
821
|
+
const prefix = parts[0];
|
|
822
|
+
const passwordId = parts[1];
|
|
823
|
+
const encryptionSalt = parts[2];
|
|
824
|
+
const encryptionIv = parts[3];
|
|
825
|
+
const encryptedB64 = parts[4];
|
|
826
|
+
const expiration = parts[5];
|
|
827
|
+
const hmacSalt = parts[6];
|
|
828
|
+
const hmac = parts[7];
|
|
829
|
+
const macBaseString = `${prefix}*${passwordId}*${encryptionSalt}*${encryptionIv}*${encryptedB64}*${expiration}`;
|
|
830
|
+
if (macPrefix !== prefix) {
|
|
831
|
+
throw new Error("Wrong mac prefix");
|
|
832
|
+
}
|
|
833
|
+
if (expiration) {
|
|
834
|
+
if (!/^\d+$/.test(expiration)) {
|
|
835
|
+
throw new Error("Invalid expiration");
|
|
836
|
+
}
|
|
837
|
+
const exp = Number.parseInt(expiration, 10);
|
|
838
|
+
if (exp <= now - opts.timestampSkewSec * 1e3) {
|
|
839
|
+
throw new Error("Expired seal");
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
if (typeof password === "undefined" || typeof password === "string" && password.length === 0) {
|
|
843
|
+
throw new Error("Empty password");
|
|
844
|
+
}
|
|
845
|
+
let pass;
|
|
846
|
+
if (typeof password === "object" && !Buffer.isBuffer(password)) {
|
|
847
|
+
if (!((passwordId || "default") in password)) {
|
|
848
|
+
throw new Error(`Cannot find password: ${passwordId}`);
|
|
849
|
+
}
|
|
850
|
+
pass = password[passwordId || "default"];
|
|
851
|
+
} else {
|
|
852
|
+
pass = password;
|
|
853
|
+
}
|
|
854
|
+
pass = normalizePassword(pass);
|
|
855
|
+
const macOptions = opts.integrity;
|
|
856
|
+
macOptions.salt = hmacSalt;
|
|
857
|
+
const mac = await hmacWithPassword(
|
|
858
|
+
_crypto,
|
|
859
|
+
pass.integrity,
|
|
860
|
+
macOptions,
|
|
861
|
+
macBaseString
|
|
862
|
+
);
|
|
863
|
+
if (!fixedTimeComparison(mac.digest, hmac)) {
|
|
864
|
+
throw new Error("Bad hmac value");
|
|
865
|
+
}
|
|
866
|
+
const encrypted = Buffer.from(encryptedB64, "base64");
|
|
867
|
+
const decryptOptions = opts.encryption;
|
|
868
|
+
decryptOptions.salt = encryptionSalt;
|
|
869
|
+
decryptOptions.iv = Buffer.from(encryptionIv, "base64");
|
|
870
|
+
const decrypted = await decrypt(
|
|
871
|
+
_crypto,
|
|
872
|
+
pass.encryption,
|
|
873
|
+
decryptOptions,
|
|
874
|
+
encrypted
|
|
875
|
+
);
|
|
876
|
+
if (decrypted) {
|
|
877
|
+
return JSON.parse(decrypted);
|
|
878
|
+
}
|
|
879
|
+
return null;
|
|
880
|
+
};
|
|
881
|
+
|
|
593
882
|
const DEFAULT_NAME = "h3";
|
|
594
883
|
const DEFAULT_COOKIE = {
|
|
595
884
|
path: "/",
|
|
@@ -632,11 +921,11 @@ async function getSession(event, config) {
|
|
|
632
921
|
session.id = (config.crypto || crypto).randomUUID();
|
|
633
922
|
await updateSession(event, config);
|
|
634
923
|
} else {
|
|
635
|
-
const unsealed = await
|
|
924
|
+
const unsealed = await unseal(
|
|
636
925
|
config.crypto || crypto,
|
|
637
926
|
reqCookie,
|
|
638
927
|
config.password,
|
|
639
|
-
config.seal ||
|
|
928
|
+
config.seal || defaults
|
|
640
929
|
);
|
|
641
930
|
Object.assign(session, unsealed);
|
|
642
931
|
}
|
|
@@ -651,11 +940,11 @@ async function updateSession(event, config, update) {
|
|
|
651
940
|
if (update) {
|
|
652
941
|
Object.assign(session.data, update);
|
|
653
942
|
}
|
|
654
|
-
const sealed = await
|
|
943
|
+
const sealed = await seal(
|
|
655
944
|
config.crypto || crypto,
|
|
656
945
|
session,
|
|
657
946
|
config.password,
|
|
658
|
-
config.seal ||
|
|
947
|
+
config.seal || defaults
|
|
659
948
|
);
|
|
660
949
|
setCookie(event, sessionName, sealed, config.cookie || DEFAULT_COOKIE);
|
|
661
950
|
return session;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,55 @@
|
|
|
1
|
-
import { SealOptions } from 'iron-webcrypto';
|
|
2
1
|
import { CookieSerializeOptions } from 'cookie-es';
|
|
3
2
|
import { IncomingMessage, ServerResponse, OutgoingMessage } from 'node:http';
|
|
4
3
|
export { IncomingMessage as NodeIncomingMessage, ServerResponse as NodeServerResponse } from 'node:http';
|
|
5
4
|
import * as ufo from 'ufo';
|
|
6
5
|
|
|
6
|
+
/**
|
|
7
|
+
* seal() method options.
|
|
8
|
+
*/
|
|
9
|
+
interface SealOptionsSub {
|
|
10
|
+
/**
|
|
11
|
+
* The length of the salt (random buffer used to ensure that two identical objects will generate a different encrypted result). Defaults to 256.
|
|
12
|
+
*/
|
|
13
|
+
saltBits: number;
|
|
14
|
+
/**
|
|
15
|
+
* The algorithm used. Defaults to 'aes-256-cbc' for encryption and 'sha256' for integrity.
|
|
16
|
+
*/
|
|
17
|
+
algorithm: "aes-128-ctr" | "aes-256-cbc" | "sha256";
|
|
18
|
+
/**
|
|
19
|
+
* The number of iterations used to derive a key from the password. Defaults to 1.
|
|
20
|
+
*/
|
|
21
|
+
iterations: number;
|
|
22
|
+
/**
|
|
23
|
+
* Minimum password size. Defaults to 32.
|
|
24
|
+
*/
|
|
25
|
+
minPasswordlength: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Options for customizing the key derivation algorithm used to generate encryption and integrity verification keys as well as the algorithms and salt sizes used.
|
|
29
|
+
*/
|
|
30
|
+
interface SealOptions {
|
|
31
|
+
/**
|
|
32
|
+
* Encryption step options.
|
|
33
|
+
*/
|
|
34
|
+
encryption: SealOptionsSub;
|
|
35
|
+
/**
|
|
36
|
+
* Integrity step options.
|
|
37
|
+
*/
|
|
38
|
+
integrity: SealOptionsSub;
|
|
39
|
+
/**
|
|
40
|
+
* Sealed object lifetime in milliseconds where 0 means forever. Defaults to 0.
|
|
41
|
+
*/
|
|
42
|
+
ttl: number;
|
|
43
|
+
/**
|
|
44
|
+
* Number of seconds of permitted clock skew for incoming expirations. Defaults to 60 seconds.
|
|
45
|
+
*/
|
|
46
|
+
timestampSkewSec: number;
|
|
47
|
+
/**
|
|
48
|
+
* Local clock time offset, expressed in number of milliseconds (positive or negative). Defaults to 0.
|
|
49
|
+
*/
|
|
50
|
+
localtimeOffsetMsec: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
7
53
|
type SessionDataT = Record<string, string | number | boolean>;
|
|
8
54
|
type SessionData<T extends SessionDataT = SessionDataT> = T;
|
|
9
55
|
interface Session<T extends SessionDataT = SessionDataT> {
|
package/dist/index.mjs
CHANGED
|
@@ -2,7 +2,6 @@ import { withoutTrailingSlash, withoutBase, getQuery as getQuery$1 } from 'ufo';
|
|
|
2
2
|
import { createRouter as createRouter$1 } from 'radix3';
|
|
3
3
|
import destr from 'destr';
|
|
4
4
|
import { parse as parse$1, serialize } from 'cookie-es';
|
|
5
|
-
import { unseal, defaults, seal } from 'iron-webcrypto';
|
|
6
5
|
import crypto from 'uncrypto';
|
|
7
6
|
|
|
8
7
|
function useBase(base, handler) {
|
|
@@ -588,6 +587,296 @@ ${header}: ${value}`;
|
|
|
588
587
|
}
|
|
589
588
|
}
|
|
590
589
|
|
|
590
|
+
const base64urlEncode = (value) => (Buffer.isBuffer(value) ? value : Buffer.from(value)).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
591
|
+
const defaults = {
|
|
592
|
+
encryption: {
|
|
593
|
+
saltBits: 256,
|
|
594
|
+
algorithm: "aes-256-cbc",
|
|
595
|
+
iterations: 1,
|
|
596
|
+
minPasswordlength: 32
|
|
597
|
+
},
|
|
598
|
+
integrity: {
|
|
599
|
+
saltBits: 256,
|
|
600
|
+
algorithm: "sha256",
|
|
601
|
+
iterations: 1,
|
|
602
|
+
minPasswordlength: 32
|
|
603
|
+
},
|
|
604
|
+
ttl: 0,
|
|
605
|
+
timestampSkewSec: 60,
|
|
606
|
+
localtimeOffsetMsec: 0
|
|
607
|
+
};
|
|
608
|
+
const clone = (options) => ({
|
|
609
|
+
...options,
|
|
610
|
+
encryption: { ...options.encryption },
|
|
611
|
+
integrity: { ...options.integrity }
|
|
612
|
+
});
|
|
613
|
+
const algorithms = {
|
|
614
|
+
"aes-128-ctr": { keyBits: 128, ivBits: 128, name: "AES-CTR" },
|
|
615
|
+
"aes-256-cbc": { keyBits: 256, ivBits: 128, name: "AES-CBC" },
|
|
616
|
+
sha256: { keyBits: 256, name: "SHA-256" }
|
|
617
|
+
};
|
|
618
|
+
const macFormatVersion = "2";
|
|
619
|
+
const macPrefix = `Fe26.${macFormatVersion}`;
|
|
620
|
+
const randomBytes = (_crypto, size) => {
|
|
621
|
+
const bytes = Buffer.allocUnsafe(size);
|
|
622
|
+
_crypto.getRandomValues(bytes);
|
|
623
|
+
return bytes;
|
|
624
|
+
};
|
|
625
|
+
const randomBits = (_crypto, bits) => {
|
|
626
|
+
if (bits < 1) {
|
|
627
|
+
throw new Error("Invalid random bits count");
|
|
628
|
+
}
|
|
629
|
+
const bytes = Math.ceil(bits / 8);
|
|
630
|
+
return randomBytes(_crypto, bytes);
|
|
631
|
+
};
|
|
632
|
+
const pbkdf2 = async (_crypto, password, salt, iterations, keyLength, hash) => {
|
|
633
|
+
const textEncoder = new TextEncoder();
|
|
634
|
+
const passwordBuffer = textEncoder.encode(password);
|
|
635
|
+
const importedKey = await _crypto.subtle.importKey(
|
|
636
|
+
"raw",
|
|
637
|
+
passwordBuffer,
|
|
638
|
+
"PBKDF2",
|
|
639
|
+
false,
|
|
640
|
+
["deriveBits"]
|
|
641
|
+
);
|
|
642
|
+
const saltBuffer = textEncoder.encode(salt);
|
|
643
|
+
const params = { name: "PBKDF2", hash, salt: saltBuffer, iterations };
|
|
644
|
+
const derivation = await _crypto.subtle.deriveBits(
|
|
645
|
+
params,
|
|
646
|
+
importedKey,
|
|
647
|
+
keyLength * 8
|
|
648
|
+
);
|
|
649
|
+
return Buffer.from(derivation);
|
|
650
|
+
};
|
|
651
|
+
const generateKey = async (_crypto, password, options) => {
|
|
652
|
+
if (password == null || password.length === 0) {
|
|
653
|
+
throw new Error("Empty password");
|
|
654
|
+
}
|
|
655
|
+
if (options == null || typeof options !== "object") {
|
|
656
|
+
throw new Error("Bad options");
|
|
657
|
+
}
|
|
658
|
+
if (!(options.algorithm in algorithms)) {
|
|
659
|
+
throw new Error(`Unknown algorithm: ${options.algorithm}`);
|
|
660
|
+
}
|
|
661
|
+
const algorithm = algorithms[options.algorithm];
|
|
662
|
+
const result = {};
|
|
663
|
+
const hmac = options.hmac ?? false;
|
|
664
|
+
const id = hmac ? { name: "HMAC", hash: algorithm.name } : { name: algorithm.name };
|
|
665
|
+
const usage = hmac ? ["sign", "verify"] : ["encrypt", "decrypt"];
|
|
666
|
+
if (typeof password === "string") {
|
|
667
|
+
if (password.length < options.minPasswordlength) {
|
|
668
|
+
throw new Error(
|
|
669
|
+
`Password string too short (min ${options.minPasswordlength} characters required)`
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
let { salt = "" } = options;
|
|
673
|
+
if (!salt) {
|
|
674
|
+
const { saltBits = 0 } = options;
|
|
675
|
+
if (!saltBits) {
|
|
676
|
+
throw new Error("Missing salt and saltBits options");
|
|
677
|
+
}
|
|
678
|
+
const randomSalt = randomBits(_crypto, saltBits);
|
|
679
|
+
salt = randomSalt.toString("hex");
|
|
680
|
+
}
|
|
681
|
+
const derivedKey = await pbkdf2(
|
|
682
|
+
_crypto,
|
|
683
|
+
password,
|
|
684
|
+
salt,
|
|
685
|
+
options.iterations,
|
|
686
|
+
algorithm.keyBits / 8,
|
|
687
|
+
"SHA-1"
|
|
688
|
+
);
|
|
689
|
+
const importedEncryptionKey = await _crypto.subtle.importKey(
|
|
690
|
+
"raw",
|
|
691
|
+
derivedKey,
|
|
692
|
+
id,
|
|
693
|
+
false,
|
|
694
|
+
usage
|
|
695
|
+
);
|
|
696
|
+
result.key = importedEncryptionKey;
|
|
697
|
+
result.salt = salt;
|
|
698
|
+
} else {
|
|
699
|
+
if (password.length < algorithm.keyBits / 8) {
|
|
700
|
+
throw new Error("Key buffer (password) too small");
|
|
701
|
+
}
|
|
702
|
+
result.key = await _crypto.subtle.importKey(
|
|
703
|
+
"raw",
|
|
704
|
+
password,
|
|
705
|
+
id,
|
|
706
|
+
false,
|
|
707
|
+
usage
|
|
708
|
+
);
|
|
709
|
+
result.salt = "";
|
|
710
|
+
}
|
|
711
|
+
if (options.iv) {
|
|
712
|
+
result.iv = options.iv;
|
|
713
|
+
} else if ("ivBits" in algorithm) {
|
|
714
|
+
result.iv = randomBits(_crypto, algorithm.ivBits);
|
|
715
|
+
}
|
|
716
|
+
return result;
|
|
717
|
+
};
|
|
718
|
+
const encrypt = async (_crypto, password, options, data) => {
|
|
719
|
+
const key = await generateKey(_crypto, password, options);
|
|
720
|
+
const textEncoder = new TextEncoder();
|
|
721
|
+
const textBuffer = textEncoder.encode(data);
|
|
722
|
+
const encrypted = await _crypto.subtle.encrypt(
|
|
723
|
+
{ name: algorithms[options.algorithm].name, iv: key.iv },
|
|
724
|
+
key.key,
|
|
725
|
+
textBuffer
|
|
726
|
+
);
|
|
727
|
+
return { encrypted: Buffer.from(encrypted), key };
|
|
728
|
+
};
|
|
729
|
+
const decrypt = async (_crypto, password, options, data) => {
|
|
730
|
+
const key = await generateKey(_crypto, password, options);
|
|
731
|
+
const decrypted = await _crypto.subtle.decrypt(
|
|
732
|
+
{ name: algorithms[options.algorithm].name, iv: key.iv },
|
|
733
|
+
key.key,
|
|
734
|
+
Buffer.isBuffer(data) ? data : Buffer.from(data)
|
|
735
|
+
);
|
|
736
|
+
const textDecoder = new TextDecoder();
|
|
737
|
+
return textDecoder.decode(decrypted);
|
|
738
|
+
};
|
|
739
|
+
const hmacWithPassword = async (_crypto, password, options, data) => {
|
|
740
|
+
const key = await generateKey(_crypto, password, { ...options, hmac: true });
|
|
741
|
+
const textEncoder = new TextEncoder();
|
|
742
|
+
const textBuffer = textEncoder.encode(data);
|
|
743
|
+
const signed = await _crypto.subtle.sign(
|
|
744
|
+
{ name: "HMAC" },
|
|
745
|
+
key.key,
|
|
746
|
+
textBuffer
|
|
747
|
+
);
|
|
748
|
+
const digest = base64urlEncode(Buffer.from(signed));
|
|
749
|
+
return { digest, salt: key.salt };
|
|
750
|
+
};
|
|
751
|
+
const normalizePassword = (password) => {
|
|
752
|
+
if (typeof password === "object" && !Buffer.isBuffer(password)) {
|
|
753
|
+
if ("secret" in password) {
|
|
754
|
+
return {
|
|
755
|
+
id: password.id,
|
|
756
|
+
encryption: password.secret,
|
|
757
|
+
integrity: password.secret
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
return {
|
|
761
|
+
id: password.id,
|
|
762
|
+
encryption: password.encryption,
|
|
763
|
+
integrity: password.integrity
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
return { encryption: password, integrity: password };
|
|
767
|
+
};
|
|
768
|
+
const seal = async (_crypto, object, password, options) => {
|
|
769
|
+
if (!password) {
|
|
770
|
+
throw new Error("Empty password");
|
|
771
|
+
}
|
|
772
|
+
const opts = clone(options);
|
|
773
|
+
const now = Date.now() + (opts.localtimeOffsetMsec || 0);
|
|
774
|
+
const objectString = JSON.stringify(object);
|
|
775
|
+
const pass = normalizePassword(password);
|
|
776
|
+
const { id = "" } = pass;
|
|
777
|
+
if (id && !/^\w+$/.test(id)) {
|
|
778
|
+
throw new Error("Invalid password id");
|
|
779
|
+
}
|
|
780
|
+
const { encrypted, key } = await encrypt(
|
|
781
|
+
_crypto,
|
|
782
|
+
pass.encryption,
|
|
783
|
+
opts.encryption,
|
|
784
|
+
objectString
|
|
785
|
+
);
|
|
786
|
+
const encryptedB64 = base64urlEncode(encrypted);
|
|
787
|
+
const iv = base64urlEncode(key.iv);
|
|
788
|
+
const expiration = opts.ttl ? now + opts.ttl : "";
|
|
789
|
+
const macBaseString = `${macPrefix}*${id}*${key.salt}*${iv}*${encryptedB64}*${expiration}`;
|
|
790
|
+
const mac = await hmacWithPassword(
|
|
791
|
+
_crypto,
|
|
792
|
+
pass.integrity,
|
|
793
|
+
opts.integrity,
|
|
794
|
+
macBaseString
|
|
795
|
+
);
|
|
796
|
+
const sealed = `${macBaseString}*${mac.salt}*${mac.digest}`;
|
|
797
|
+
return sealed;
|
|
798
|
+
};
|
|
799
|
+
const fixedTimeComparison = (a, b) => {
|
|
800
|
+
let mismatch = a.length === b.length ? 0 : 1;
|
|
801
|
+
if (mismatch) {
|
|
802
|
+
b = a;
|
|
803
|
+
}
|
|
804
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
805
|
+
mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
806
|
+
}
|
|
807
|
+
return mismatch === 0;
|
|
808
|
+
};
|
|
809
|
+
const unseal = async (_crypto, sealed, password, options) => {
|
|
810
|
+
if (!password) {
|
|
811
|
+
throw new Error("Empty password");
|
|
812
|
+
}
|
|
813
|
+
const opts = clone(options);
|
|
814
|
+
const now = Date.now() + (opts.localtimeOffsetMsec || 0);
|
|
815
|
+
const parts = sealed.split("*");
|
|
816
|
+
if (parts.length !== 8) {
|
|
817
|
+
throw new Error("Incorrect number of sealed components");
|
|
818
|
+
}
|
|
819
|
+
const prefix = parts[0];
|
|
820
|
+
const passwordId = parts[1];
|
|
821
|
+
const encryptionSalt = parts[2];
|
|
822
|
+
const encryptionIv = parts[3];
|
|
823
|
+
const encryptedB64 = parts[4];
|
|
824
|
+
const expiration = parts[5];
|
|
825
|
+
const hmacSalt = parts[6];
|
|
826
|
+
const hmac = parts[7];
|
|
827
|
+
const macBaseString = `${prefix}*${passwordId}*${encryptionSalt}*${encryptionIv}*${encryptedB64}*${expiration}`;
|
|
828
|
+
if (macPrefix !== prefix) {
|
|
829
|
+
throw new Error("Wrong mac prefix");
|
|
830
|
+
}
|
|
831
|
+
if (expiration) {
|
|
832
|
+
if (!/^\d+$/.test(expiration)) {
|
|
833
|
+
throw new Error("Invalid expiration");
|
|
834
|
+
}
|
|
835
|
+
const exp = Number.parseInt(expiration, 10);
|
|
836
|
+
if (exp <= now - opts.timestampSkewSec * 1e3) {
|
|
837
|
+
throw new Error("Expired seal");
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
if (typeof password === "undefined" || typeof password === "string" && password.length === 0) {
|
|
841
|
+
throw new Error("Empty password");
|
|
842
|
+
}
|
|
843
|
+
let pass;
|
|
844
|
+
if (typeof password === "object" && !Buffer.isBuffer(password)) {
|
|
845
|
+
if (!((passwordId || "default") in password)) {
|
|
846
|
+
throw new Error(`Cannot find password: ${passwordId}`);
|
|
847
|
+
}
|
|
848
|
+
pass = password[passwordId || "default"];
|
|
849
|
+
} else {
|
|
850
|
+
pass = password;
|
|
851
|
+
}
|
|
852
|
+
pass = normalizePassword(pass);
|
|
853
|
+
const macOptions = opts.integrity;
|
|
854
|
+
macOptions.salt = hmacSalt;
|
|
855
|
+
const mac = await hmacWithPassword(
|
|
856
|
+
_crypto,
|
|
857
|
+
pass.integrity,
|
|
858
|
+
macOptions,
|
|
859
|
+
macBaseString
|
|
860
|
+
);
|
|
861
|
+
if (!fixedTimeComparison(mac.digest, hmac)) {
|
|
862
|
+
throw new Error("Bad hmac value");
|
|
863
|
+
}
|
|
864
|
+
const encrypted = Buffer.from(encryptedB64, "base64");
|
|
865
|
+
const decryptOptions = opts.encryption;
|
|
866
|
+
decryptOptions.salt = encryptionSalt;
|
|
867
|
+
decryptOptions.iv = Buffer.from(encryptionIv, "base64");
|
|
868
|
+
const decrypted = await decrypt(
|
|
869
|
+
_crypto,
|
|
870
|
+
pass.encryption,
|
|
871
|
+
decryptOptions,
|
|
872
|
+
encrypted
|
|
873
|
+
);
|
|
874
|
+
if (decrypted) {
|
|
875
|
+
return JSON.parse(decrypted);
|
|
876
|
+
}
|
|
877
|
+
return null;
|
|
878
|
+
};
|
|
879
|
+
|
|
591
880
|
const DEFAULT_NAME = "h3";
|
|
592
881
|
const DEFAULT_COOKIE = {
|
|
593
882
|
path: "/",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "h3",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "Tiny JavaScript Server",
|
|
5
5
|
"repository": "unjs/h3",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,7 +22,6 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"cookie-es": "^0.5.0",
|
|
24
24
|
"destr": "^1.2.2",
|
|
25
|
-
"iron-webcrypto": "^0.2.7",
|
|
26
25
|
"radix3": "^1.0.0",
|
|
27
26
|
"ufo": "^1.0.1",
|
|
28
27
|
"uncrypto": "^0.1.2"
|