@excofy/utils 1.0.12 → 1.0.14
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/.secrets.sample +1 -0
- package/dist/index.cjs +143 -0
- package/dist/index.d.cts +44 -1
- package/dist/index.d.ts +44 -1
- package/dist/index.js +141 -0
- package/package.json +4 -2
- package/src/helpers/crypto.ts +202 -0
- package/src/helpers/generateCode.ts +26 -0
- package/src/index.ts +4 -0
- package/tests/helpers/webCrypto.spec.ts +126 -0
- package/tsconfig.json +1 -1
package/.secrets.sample
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
NPM_TOKEN=
|
package/dist/index.cjs
CHANGED
|
@@ -31,6 +31,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
createValidator: () => createValidator,
|
|
34
|
+
cryptoUtils: () => cryptoUtils,
|
|
35
|
+
generateCode: () => generateCode,
|
|
34
36
|
htmlEntityDecode: () => htmlEntityDecode,
|
|
35
37
|
numberUtils: () => number_exports,
|
|
36
38
|
slugUtils: () => slug_exports,
|
|
@@ -644,11 +646,150 @@ function createValidator() {
|
|
|
644
646
|
};
|
|
645
647
|
}
|
|
646
648
|
|
|
649
|
+
// src/helpers/generateCode.ts
|
|
650
|
+
var generateCode = (length = 6, alphanumeric = true) => {
|
|
651
|
+
const digits = "0123456789";
|
|
652
|
+
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
653
|
+
const charset = alphanumeric ? digits + letters : digits;
|
|
654
|
+
let result = "";
|
|
655
|
+
for (let i = 0; i < length; i++) {
|
|
656
|
+
const randomIndex = Math.floor(Math.random() * charset.length);
|
|
657
|
+
result += charset[randomIndex];
|
|
658
|
+
}
|
|
659
|
+
return result;
|
|
660
|
+
};
|
|
661
|
+
|
|
647
662
|
// src/helpers/string.ts
|
|
648
663
|
var stringUtils = {
|
|
649
664
|
removeFileExtension: (fileName) => fileName.replace(/\.[^/.]+$/, "")
|
|
650
665
|
};
|
|
651
666
|
|
|
667
|
+
// src/helpers/crypto.ts
|
|
668
|
+
var encoder = new TextEncoder();
|
|
669
|
+
var decoder = new TextDecoder();
|
|
670
|
+
var toBase64 = (buffer) => btoa(String.fromCharCode(...new Uint8Array(buffer)));
|
|
671
|
+
var fromBase64 = (str) => Uint8Array.from(atob(str), (c) => c.charCodeAt(0));
|
|
672
|
+
var signatureKey = async (AUTH_SIGN_SECRET) => crypto.subtle.importKey(
|
|
673
|
+
"raw",
|
|
674
|
+
encoder.encode(AUTH_SIGN_SECRET),
|
|
675
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
676
|
+
false,
|
|
677
|
+
["sign", "verify"]
|
|
678
|
+
);
|
|
679
|
+
var payloadKey = async (AUTH_PAYLOAD_SECRET) => crypto.subtle.importKey(
|
|
680
|
+
"raw",
|
|
681
|
+
encoder.encode(AUTH_PAYLOAD_SECRET.substring(0, 32)),
|
|
682
|
+
{ name: "AES-GCM", length: 256 },
|
|
683
|
+
true,
|
|
684
|
+
["encrypt", "decrypt"]
|
|
685
|
+
);
|
|
686
|
+
var encryptPayload = async ({
|
|
687
|
+
AUTH_PAYLOAD_SECRET,
|
|
688
|
+
payload
|
|
689
|
+
}) => {
|
|
690
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
691
|
+
const key = await payloadKey(AUTH_PAYLOAD_SECRET);
|
|
692
|
+
const buffer = await crypto.subtle.encrypt(
|
|
693
|
+
{ name: "AES-GCM", iv },
|
|
694
|
+
key,
|
|
695
|
+
encoder.encode(payload)
|
|
696
|
+
);
|
|
697
|
+
return `${toBase64(iv)}.${toBase64(buffer)}`;
|
|
698
|
+
};
|
|
699
|
+
var decryptPayload = async ({
|
|
700
|
+
AUTH_PAYLOAD_SECRET,
|
|
701
|
+
token
|
|
702
|
+
}) => {
|
|
703
|
+
const [header, payload] = token.split(".");
|
|
704
|
+
const key = await payloadKey(AUTH_PAYLOAD_SECRET);
|
|
705
|
+
const buffer = await crypto.subtle.decrypt(
|
|
706
|
+
{ name: "AES-GCM", iv: fromBase64(header).buffer },
|
|
707
|
+
key,
|
|
708
|
+
fromBase64(payload).buffer
|
|
709
|
+
);
|
|
710
|
+
return decoder.decode(buffer);
|
|
711
|
+
};
|
|
712
|
+
var cryptoUtils = {
|
|
713
|
+
uuidV4: () => crypto.randomUUID(),
|
|
714
|
+
hash: async (password, salt) => {
|
|
715
|
+
const importedKey = await crypto.subtle.importKey(
|
|
716
|
+
"raw",
|
|
717
|
+
encoder.encode(password),
|
|
718
|
+
{ name: "PBKDF2" },
|
|
719
|
+
false,
|
|
720
|
+
["deriveBits"]
|
|
721
|
+
);
|
|
722
|
+
const derivedKey = await crypto.subtle.deriveBits(
|
|
723
|
+
{
|
|
724
|
+
name: "PBKDF2",
|
|
725
|
+
salt: encoder.encode(String(salt)),
|
|
726
|
+
iterations: 1e3,
|
|
727
|
+
hash: "SHA-256"
|
|
728
|
+
},
|
|
729
|
+
importedKey,
|
|
730
|
+
256
|
|
731
|
+
);
|
|
732
|
+
return Array.from(new Uint8Array(derivedKey)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
733
|
+
},
|
|
734
|
+
isMatch: async (password, hash, salt = 10) => {
|
|
735
|
+
const hashed = await cryptoUtils.hash(password, salt);
|
|
736
|
+
return hashed === hash;
|
|
737
|
+
},
|
|
738
|
+
generateAccessToken: async ({
|
|
739
|
+
AUTH_PAYLOAD_SECRET,
|
|
740
|
+
AUTH_SIGN_SECRET,
|
|
741
|
+
payload
|
|
742
|
+
}) => {
|
|
743
|
+
const key = await signatureKey(AUTH_SIGN_SECRET);
|
|
744
|
+
const payloadEncrypted = await encryptPayload({
|
|
745
|
+
AUTH_PAYLOAD_SECRET,
|
|
746
|
+
payload
|
|
747
|
+
});
|
|
748
|
+
const signatureBuffer = await crypto.subtle.sign(
|
|
749
|
+
"HMAC",
|
|
750
|
+
key,
|
|
751
|
+
encoder.encode(payload)
|
|
752
|
+
);
|
|
753
|
+
const signature = toBase64(signatureBuffer);
|
|
754
|
+
return `${payloadEncrypted}.${signature}`;
|
|
755
|
+
},
|
|
756
|
+
validateAccessToken: async ({
|
|
757
|
+
AUTH_PAYLOAD_SECRET,
|
|
758
|
+
AUTH_SIGN_SECRET,
|
|
759
|
+
accessToken
|
|
760
|
+
}) => {
|
|
761
|
+
let payloadDecrypted;
|
|
762
|
+
try {
|
|
763
|
+
const [header, payload, signature] = accessToken.split(".");
|
|
764
|
+
payloadDecrypted = await decryptPayload({
|
|
765
|
+
AUTH_PAYLOAD_SECRET,
|
|
766
|
+
token: `${header}.${payload}`
|
|
767
|
+
});
|
|
768
|
+
const key = await signatureKey(AUTH_SIGN_SECRET);
|
|
769
|
+
const valid = await crypto.subtle.verify(
|
|
770
|
+
"HMAC",
|
|
771
|
+
key,
|
|
772
|
+
fromBase64(signature).buffer,
|
|
773
|
+
encoder.encode(payloadDecrypted)
|
|
774
|
+
);
|
|
775
|
+
if (!valid) throw new Error("Invalid access token");
|
|
776
|
+
} catch (err) {
|
|
777
|
+
throw new Error("Invalid access token");
|
|
778
|
+
}
|
|
779
|
+
const { expiresAt } = JSON.parse(payloadDecrypted);
|
|
780
|
+
if (Date.now() > new Date(expiresAt).getTime()) {
|
|
781
|
+
throw new Error("Expired access token");
|
|
782
|
+
}
|
|
783
|
+
return payloadDecrypted;
|
|
784
|
+
},
|
|
785
|
+
sha256ToHex: async (data) => {
|
|
786
|
+
const buffer = await crypto.subtle.digest("SHA-256", encoder.encode(data));
|
|
787
|
+
return Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
788
|
+
},
|
|
789
|
+
encryptPayload,
|
|
790
|
+
decryptPayload
|
|
791
|
+
};
|
|
792
|
+
|
|
652
793
|
// src/helpers/slug.ts
|
|
653
794
|
var slug_exports = {};
|
|
654
795
|
__export(slug_exports, {
|
|
@@ -689,6 +830,8 @@ var toDecimal = (value) => {
|
|
|
689
830
|
// Annotate the CommonJS export names for ESM import in node:
|
|
690
831
|
0 && (module.exports = {
|
|
691
832
|
createValidator,
|
|
833
|
+
cryptoUtils,
|
|
834
|
+
generateCode,
|
|
692
835
|
htmlEntityDecode,
|
|
693
836
|
numberUtils,
|
|
694
837
|
slugUtils,
|
package/dist/index.d.cts
CHANGED
|
@@ -85,10 +85,53 @@ declare function createValidator<TRaw extends Record<string, TInputValue>, TPars
|
|
|
85
85
|
};
|
|
86
86
|
};
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Gera um código de validação.
|
|
90
|
+
*
|
|
91
|
+
* @param {number} [length=6] - Número de caracteres do código.
|
|
92
|
+
* @param {boolean} [alphanumeric=true] - Se `true`, gera código alfanumérico (letras e números).
|
|
93
|
+
* Se `false`, gera apenas dígitos numéricos.
|
|
94
|
+
* @returns {string} Código gerado.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* generateCode(); // "482901"
|
|
98
|
+
* generateCode(8); // "83910274"
|
|
99
|
+
* generateCode(8, true); // "A9F3D2K7"
|
|
100
|
+
*/
|
|
101
|
+
declare const generateCode: (length?: number, alphanumeric?: boolean) => string;
|
|
102
|
+
|
|
88
103
|
declare const stringUtils: {
|
|
89
104
|
removeFileExtension: (fileName: string) => string;
|
|
90
105
|
};
|
|
91
106
|
|
|
107
|
+
interface IGenerateAccessToken {
|
|
108
|
+
AUTH_SIGN_SECRET: string;
|
|
109
|
+
AUTH_PAYLOAD_SECRET: string;
|
|
110
|
+
payload: string;
|
|
111
|
+
}
|
|
112
|
+
interface IValidateAccessToken {
|
|
113
|
+
AUTH_SIGN_SECRET: string;
|
|
114
|
+
AUTH_PAYLOAD_SECRET: string;
|
|
115
|
+
accessToken: string;
|
|
116
|
+
}
|
|
117
|
+
interface ICrypto {
|
|
118
|
+
uuidV4: () => string;
|
|
119
|
+
hash: (password: string, salt: number) => Promise<string>;
|
|
120
|
+
isMatch: (password: string, hash: string, salt?: number) => Promise<boolean>;
|
|
121
|
+
generateAccessToken: (data: IGenerateAccessToken) => Promise<string>;
|
|
122
|
+
validateAccessToken: (data: IValidateAccessToken) => Promise<string>;
|
|
123
|
+
sha256ToHex: (data: string) => Promise<string>;
|
|
124
|
+
encryptPayload: (data: {
|
|
125
|
+
AUTH_PAYLOAD_SECRET: string;
|
|
126
|
+
payload: string;
|
|
127
|
+
}) => Promise<string>;
|
|
128
|
+
decryptPayload: (data: {
|
|
129
|
+
AUTH_PAYLOAD_SECRET: string;
|
|
130
|
+
token: string;
|
|
131
|
+
}) => Promise<string>;
|
|
132
|
+
}
|
|
133
|
+
declare const cryptoUtils: ICrypto;
|
|
134
|
+
|
|
92
135
|
/**
|
|
93
136
|
* Generates a unique slug by appending a numeric suffix to the base slug if necessary.
|
|
94
137
|
*
|
|
@@ -151,4 +194,4 @@ declare namespace number {
|
|
|
151
194
|
export { number_toCents as toCents, number_toDecimal as toDecimal };
|
|
152
195
|
}
|
|
153
196
|
|
|
154
|
-
export { createValidator, htmlEntityDecode, number as numberUtils, slug as slugUtils, stringUtils };
|
|
197
|
+
export { createValidator, cryptoUtils, generateCode, htmlEntityDecode, number as numberUtils, slug as slugUtils, stringUtils };
|
package/dist/index.d.ts
CHANGED
|
@@ -85,10 +85,53 @@ declare function createValidator<TRaw extends Record<string, TInputValue>, TPars
|
|
|
85
85
|
};
|
|
86
86
|
};
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Gera um código de validação.
|
|
90
|
+
*
|
|
91
|
+
* @param {number} [length=6] - Número de caracteres do código.
|
|
92
|
+
* @param {boolean} [alphanumeric=true] - Se `true`, gera código alfanumérico (letras e números).
|
|
93
|
+
* Se `false`, gera apenas dígitos numéricos.
|
|
94
|
+
* @returns {string} Código gerado.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* generateCode(); // "482901"
|
|
98
|
+
* generateCode(8); // "83910274"
|
|
99
|
+
* generateCode(8, true); // "A9F3D2K7"
|
|
100
|
+
*/
|
|
101
|
+
declare const generateCode: (length?: number, alphanumeric?: boolean) => string;
|
|
102
|
+
|
|
88
103
|
declare const stringUtils: {
|
|
89
104
|
removeFileExtension: (fileName: string) => string;
|
|
90
105
|
};
|
|
91
106
|
|
|
107
|
+
interface IGenerateAccessToken {
|
|
108
|
+
AUTH_SIGN_SECRET: string;
|
|
109
|
+
AUTH_PAYLOAD_SECRET: string;
|
|
110
|
+
payload: string;
|
|
111
|
+
}
|
|
112
|
+
interface IValidateAccessToken {
|
|
113
|
+
AUTH_SIGN_SECRET: string;
|
|
114
|
+
AUTH_PAYLOAD_SECRET: string;
|
|
115
|
+
accessToken: string;
|
|
116
|
+
}
|
|
117
|
+
interface ICrypto {
|
|
118
|
+
uuidV4: () => string;
|
|
119
|
+
hash: (password: string, salt: number) => Promise<string>;
|
|
120
|
+
isMatch: (password: string, hash: string, salt?: number) => Promise<boolean>;
|
|
121
|
+
generateAccessToken: (data: IGenerateAccessToken) => Promise<string>;
|
|
122
|
+
validateAccessToken: (data: IValidateAccessToken) => Promise<string>;
|
|
123
|
+
sha256ToHex: (data: string) => Promise<string>;
|
|
124
|
+
encryptPayload: (data: {
|
|
125
|
+
AUTH_PAYLOAD_SECRET: string;
|
|
126
|
+
payload: string;
|
|
127
|
+
}) => Promise<string>;
|
|
128
|
+
decryptPayload: (data: {
|
|
129
|
+
AUTH_PAYLOAD_SECRET: string;
|
|
130
|
+
token: string;
|
|
131
|
+
}) => Promise<string>;
|
|
132
|
+
}
|
|
133
|
+
declare const cryptoUtils: ICrypto;
|
|
134
|
+
|
|
92
135
|
/**
|
|
93
136
|
* Generates a unique slug by appending a numeric suffix to the base slug if necessary.
|
|
94
137
|
*
|
|
@@ -151,4 +194,4 @@ declare namespace number {
|
|
|
151
194
|
export { number_toCents as toCents, number_toDecimal as toDecimal };
|
|
152
195
|
}
|
|
153
196
|
|
|
154
|
-
export { createValidator, htmlEntityDecode, number as numberUtils, slug as slugUtils, stringUtils };
|
|
197
|
+
export { createValidator, cryptoUtils, generateCode, htmlEntityDecode, number as numberUtils, slug as slugUtils, stringUtils };
|
package/dist/index.js
CHANGED
|
@@ -610,11 +610,150 @@ function createValidator() {
|
|
|
610
610
|
};
|
|
611
611
|
}
|
|
612
612
|
|
|
613
|
+
// src/helpers/generateCode.ts
|
|
614
|
+
var generateCode = (length = 6, alphanumeric = true) => {
|
|
615
|
+
const digits = "0123456789";
|
|
616
|
+
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
617
|
+
const charset = alphanumeric ? digits + letters : digits;
|
|
618
|
+
let result = "";
|
|
619
|
+
for (let i = 0; i < length; i++) {
|
|
620
|
+
const randomIndex = Math.floor(Math.random() * charset.length);
|
|
621
|
+
result += charset[randomIndex];
|
|
622
|
+
}
|
|
623
|
+
return result;
|
|
624
|
+
};
|
|
625
|
+
|
|
613
626
|
// src/helpers/string.ts
|
|
614
627
|
var stringUtils = {
|
|
615
628
|
removeFileExtension: (fileName) => fileName.replace(/\.[^/.]+$/, "")
|
|
616
629
|
};
|
|
617
630
|
|
|
631
|
+
// src/helpers/crypto.ts
|
|
632
|
+
var encoder = new TextEncoder();
|
|
633
|
+
var decoder = new TextDecoder();
|
|
634
|
+
var toBase64 = (buffer) => btoa(String.fromCharCode(...new Uint8Array(buffer)));
|
|
635
|
+
var fromBase64 = (str) => Uint8Array.from(atob(str), (c) => c.charCodeAt(0));
|
|
636
|
+
var signatureKey = async (AUTH_SIGN_SECRET) => crypto.subtle.importKey(
|
|
637
|
+
"raw",
|
|
638
|
+
encoder.encode(AUTH_SIGN_SECRET),
|
|
639
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
640
|
+
false,
|
|
641
|
+
["sign", "verify"]
|
|
642
|
+
);
|
|
643
|
+
var payloadKey = async (AUTH_PAYLOAD_SECRET) => crypto.subtle.importKey(
|
|
644
|
+
"raw",
|
|
645
|
+
encoder.encode(AUTH_PAYLOAD_SECRET.substring(0, 32)),
|
|
646
|
+
{ name: "AES-GCM", length: 256 },
|
|
647
|
+
true,
|
|
648
|
+
["encrypt", "decrypt"]
|
|
649
|
+
);
|
|
650
|
+
var encryptPayload = async ({
|
|
651
|
+
AUTH_PAYLOAD_SECRET,
|
|
652
|
+
payload
|
|
653
|
+
}) => {
|
|
654
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
655
|
+
const key = await payloadKey(AUTH_PAYLOAD_SECRET);
|
|
656
|
+
const buffer = await crypto.subtle.encrypt(
|
|
657
|
+
{ name: "AES-GCM", iv },
|
|
658
|
+
key,
|
|
659
|
+
encoder.encode(payload)
|
|
660
|
+
);
|
|
661
|
+
return `${toBase64(iv)}.${toBase64(buffer)}`;
|
|
662
|
+
};
|
|
663
|
+
var decryptPayload = async ({
|
|
664
|
+
AUTH_PAYLOAD_SECRET,
|
|
665
|
+
token
|
|
666
|
+
}) => {
|
|
667
|
+
const [header, payload] = token.split(".");
|
|
668
|
+
const key = await payloadKey(AUTH_PAYLOAD_SECRET);
|
|
669
|
+
const buffer = await crypto.subtle.decrypt(
|
|
670
|
+
{ name: "AES-GCM", iv: fromBase64(header).buffer },
|
|
671
|
+
key,
|
|
672
|
+
fromBase64(payload).buffer
|
|
673
|
+
);
|
|
674
|
+
return decoder.decode(buffer);
|
|
675
|
+
};
|
|
676
|
+
var cryptoUtils = {
|
|
677
|
+
uuidV4: () => crypto.randomUUID(),
|
|
678
|
+
hash: async (password, salt) => {
|
|
679
|
+
const importedKey = await crypto.subtle.importKey(
|
|
680
|
+
"raw",
|
|
681
|
+
encoder.encode(password),
|
|
682
|
+
{ name: "PBKDF2" },
|
|
683
|
+
false,
|
|
684
|
+
["deriveBits"]
|
|
685
|
+
);
|
|
686
|
+
const derivedKey = await crypto.subtle.deriveBits(
|
|
687
|
+
{
|
|
688
|
+
name: "PBKDF2",
|
|
689
|
+
salt: encoder.encode(String(salt)),
|
|
690
|
+
iterations: 1e3,
|
|
691
|
+
hash: "SHA-256"
|
|
692
|
+
},
|
|
693
|
+
importedKey,
|
|
694
|
+
256
|
|
695
|
+
);
|
|
696
|
+
return Array.from(new Uint8Array(derivedKey)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
697
|
+
},
|
|
698
|
+
isMatch: async (password, hash, salt = 10) => {
|
|
699
|
+
const hashed = await cryptoUtils.hash(password, salt);
|
|
700
|
+
return hashed === hash;
|
|
701
|
+
},
|
|
702
|
+
generateAccessToken: async ({
|
|
703
|
+
AUTH_PAYLOAD_SECRET,
|
|
704
|
+
AUTH_SIGN_SECRET,
|
|
705
|
+
payload
|
|
706
|
+
}) => {
|
|
707
|
+
const key = await signatureKey(AUTH_SIGN_SECRET);
|
|
708
|
+
const payloadEncrypted = await encryptPayload({
|
|
709
|
+
AUTH_PAYLOAD_SECRET,
|
|
710
|
+
payload
|
|
711
|
+
});
|
|
712
|
+
const signatureBuffer = await crypto.subtle.sign(
|
|
713
|
+
"HMAC",
|
|
714
|
+
key,
|
|
715
|
+
encoder.encode(payload)
|
|
716
|
+
);
|
|
717
|
+
const signature = toBase64(signatureBuffer);
|
|
718
|
+
return `${payloadEncrypted}.${signature}`;
|
|
719
|
+
},
|
|
720
|
+
validateAccessToken: async ({
|
|
721
|
+
AUTH_PAYLOAD_SECRET,
|
|
722
|
+
AUTH_SIGN_SECRET,
|
|
723
|
+
accessToken
|
|
724
|
+
}) => {
|
|
725
|
+
let payloadDecrypted;
|
|
726
|
+
try {
|
|
727
|
+
const [header, payload, signature] = accessToken.split(".");
|
|
728
|
+
payloadDecrypted = await decryptPayload({
|
|
729
|
+
AUTH_PAYLOAD_SECRET,
|
|
730
|
+
token: `${header}.${payload}`
|
|
731
|
+
});
|
|
732
|
+
const key = await signatureKey(AUTH_SIGN_SECRET);
|
|
733
|
+
const valid = await crypto.subtle.verify(
|
|
734
|
+
"HMAC",
|
|
735
|
+
key,
|
|
736
|
+
fromBase64(signature).buffer,
|
|
737
|
+
encoder.encode(payloadDecrypted)
|
|
738
|
+
);
|
|
739
|
+
if (!valid) throw new Error("Invalid access token");
|
|
740
|
+
} catch (err) {
|
|
741
|
+
throw new Error("Invalid access token");
|
|
742
|
+
}
|
|
743
|
+
const { expiresAt } = JSON.parse(payloadDecrypted);
|
|
744
|
+
if (Date.now() > new Date(expiresAt).getTime()) {
|
|
745
|
+
throw new Error("Expired access token");
|
|
746
|
+
}
|
|
747
|
+
return payloadDecrypted;
|
|
748
|
+
},
|
|
749
|
+
sha256ToHex: async (data) => {
|
|
750
|
+
const buffer = await crypto.subtle.digest("SHA-256", encoder.encode(data));
|
|
751
|
+
return Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
752
|
+
},
|
|
753
|
+
encryptPayload,
|
|
754
|
+
decryptPayload
|
|
755
|
+
};
|
|
756
|
+
|
|
618
757
|
// src/helpers/slug.ts
|
|
619
758
|
var slug_exports = {};
|
|
620
759
|
__export(slug_exports, {
|
|
@@ -654,6 +793,8 @@ var toDecimal = (value) => {
|
|
|
654
793
|
};
|
|
655
794
|
export {
|
|
656
795
|
createValidator,
|
|
796
|
+
cryptoUtils,
|
|
797
|
+
generateCode,
|
|
657
798
|
htmlEntityDecode,
|
|
658
799
|
number_exports as numberUtils,
|
|
659
800
|
slug_exports as slugUtils,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@excofy/utils",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.14",
|
|
4
4
|
"description": "Biblioteca de utilitários para o Excofy",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"lint": "biome lint",
|
|
16
16
|
"typecheck": "tsc --noEmit",
|
|
17
17
|
"prepublishOnly": "npm run build",
|
|
18
|
-
"deploy": "act"
|
|
18
|
+
"deploy": "act",
|
|
19
|
+
"test": "tsx --test 'tests/**/*.spec.ts' --testTimeout=10000"
|
|
19
20
|
},
|
|
20
21
|
"repository": {
|
|
21
22
|
"type": "git",
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
"@types/big.js": "^6.2.2",
|
|
34
35
|
"@types/node": "^24.0.3",
|
|
35
36
|
"tsup": "^8.5.0",
|
|
37
|
+
"tsx": "^4.20.4",
|
|
36
38
|
"typescript": "^5.8.3"
|
|
37
39
|
},
|
|
38
40
|
"dependencies": {
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
interface IGenerateAccessToken {
|
|
2
|
+
AUTH_SIGN_SECRET: string;
|
|
3
|
+
AUTH_PAYLOAD_SECRET: string;
|
|
4
|
+
payload: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface IValidateAccessToken {
|
|
8
|
+
AUTH_SIGN_SECRET: string;
|
|
9
|
+
AUTH_PAYLOAD_SECRET: string;
|
|
10
|
+
accessToken: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ICrypto {
|
|
14
|
+
uuidV4: () => string;
|
|
15
|
+
hash: (password: string, salt: number) => Promise<string>;
|
|
16
|
+
isMatch: (password: string, hash: string, salt?: number) => Promise<boolean>;
|
|
17
|
+
generateAccessToken: (data: IGenerateAccessToken) => Promise<string>;
|
|
18
|
+
validateAccessToken: (data: IValidateAccessToken) => Promise<string>;
|
|
19
|
+
sha256ToHex: (data: string) => Promise<string>;
|
|
20
|
+
encryptPayload: (data: {
|
|
21
|
+
AUTH_PAYLOAD_SECRET: string;
|
|
22
|
+
payload: string;
|
|
23
|
+
}) => Promise<string>;
|
|
24
|
+
decryptPayload: (data: {
|
|
25
|
+
AUTH_PAYLOAD_SECRET: string;
|
|
26
|
+
token: string;
|
|
27
|
+
}) => Promise<string>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* --------------------- Helpers básicos --------------------- */
|
|
31
|
+
const encoder = new TextEncoder();
|
|
32
|
+
const decoder = new TextDecoder();
|
|
33
|
+
|
|
34
|
+
const toBase64 = (buffer: ArrayBuffer | Uint8Array): string =>
|
|
35
|
+
btoa(String.fromCharCode(...new Uint8Array(buffer)));
|
|
36
|
+
|
|
37
|
+
const fromBase64 = (str: string): Uint8Array =>
|
|
38
|
+
Uint8Array.from(atob(str), (c) => c.charCodeAt(0));
|
|
39
|
+
|
|
40
|
+
const signatureKey = async (AUTH_SIGN_SECRET: string) =>
|
|
41
|
+
crypto.subtle.importKey(
|
|
42
|
+
'raw',
|
|
43
|
+
encoder.encode(AUTH_SIGN_SECRET),
|
|
44
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
45
|
+
false,
|
|
46
|
+
['sign', 'verify']
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const payloadKey = async (AUTH_PAYLOAD_SECRET: string) =>
|
|
50
|
+
crypto.subtle.importKey(
|
|
51
|
+
'raw',
|
|
52
|
+
encoder.encode(AUTH_PAYLOAD_SECRET.substring(0, 32)),
|
|
53
|
+
{ name: 'AES-GCM', length: 256 },
|
|
54
|
+
true,
|
|
55
|
+
['encrypt', 'decrypt']
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
/* --------------------- Payload (encrypt/decrypt) --------------------- */
|
|
59
|
+
const encryptPayload = async ({
|
|
60
|
+
AUTH_PAYLOAD_SECRET,
|
|
61
|
+
payload,
|
|
62
|
+
}: {
|
|
63
|
+
AUTH_PAYLOAD_SECRET: string;
|
|
64
|
+
payload: string;
|
|
65
|
+
}): Promise<string> => {
|
|
66
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
67
|
+
const key = await payloadKey(AUTH_PAYLOAD_SECRET);
|
|
68
|
+
|
|
69
|
+
const buffer = await crypto.subtle.encrypt(
|
|
70
|
+
{ name: 'AES-GCM', iv },
|
|
71
|
+
key,
|
|
72
|
+
encoder.encode(payload)
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
return `${toBase64(iv)}.${toBase64(buffer)}`;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const decryptPayload = async ({
|
|
79
|
+
AUTH_PAYLOAD_SECRET,
|
|
80
|
+
token,
|
|
81
|
+
}: {
|
|
82
|
+
AUTH_PAYLOAD_SECRET: string;
|
|
83
|
+
token: string;
|
|
84
|
+
}): Promise<string> => {
|
|
85
|
+
const [header, payload] = token.split('.');
|
|
86
|
+
const key = await payloadKey(AUTH_PAYLOAD_SECRET);
|
|
87
|
+
|
|
88
|
+
const buffer = await crypto.subtle.decrypt(
|
|
89
|
+
{ name: 'AES-GCM', iv: fromBase64(header).buffer as ArrayBuffer },
|
|
90
|
+
key,
|
|
91
|
+
fromBase64(payload).buffer as ArrayBuffer
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return decoder.decode(buffer);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/* --------------------- Main API --------------------- */
|
|
98
|
+
export const cryptoUtils: ICrypto = {
|
|
99
|
+
uuidV4: (): string => crypto.randomUUID(),
|
|
100
|
+
|
|
101
|
+
hash: async (password: string, salt: number): Promise<string> => {
|
|
102
|
+
const importedKey = await crypto.subtle.importKey(
|
|
103
|
+
'raw',
|
|
104
|
+
encoder.encode(password),
|
|
105
|
+
{ name: 'PBKDF2' },
|
|
106
|
+
false,
|
|
107
|
+
['deriveBits']
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const derivedKey = await crypto.subtle.deriveBits(
|
|
111
|
+
{
|
|
112
|
+
name: 'PBKDF2',
|
|
113
|
+
salt: encoder.encode(String(salt)),
|
|
114
|
+
iterations: 1000,
|
|
115
|
+
hash: 'SHA-256',
|
|
116
|
+
},
|
|
117
|
+
importedKey,
|
|
118
|
+
256
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
return Array.from(new Uint8Array(derivedKey))
|
|
122
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
123
|
+
.join('');
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
isMatch: async (password, hash, salt = 10): Promise<boolean> => {
|
|
127
|
+
const hashed = await cryptoUtils.hash(password, salt);
|
|
128
|
+
return hashed === hash;
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
generateAccessToken: async ({
|
|
132
|
+
AUTH_PAYLOAD_SECRET,
|
|
133
|
+
AUTH_SIGN_SECRET,
|
|
134
|
+
payload,
|
|
135
|
+
}) => {
|
|
136
|
+
const key = await signatureKey(AUTH_SIGN_SECRET);
|
|
137
|
+
|
|
138
|
+
// cifra payload
|
|
139
|
+
const payloadEncrypted = await encryptPayload({
|
|
140
|
+
AUTH_PAYLOAD_SECRET,
|
|
141
|
+
payload,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// assinatura
|
|
145
|
+
const signatureBuffer = await crypto.subtle.sign(
|
|
146
|
+
'HMAC',
|
|
147
|
+
key,
|
|
148
|
+
encoder.encode(payload)
|
|
149
|
+
);
|
|
150
|
+
const signature = toBase64(signatureBuffer);
|
|
151
|
+
|
|
152
|
+
return `${payloadEncrypted}.${signature}`;
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
validateAccessToken: async ({
|
|
156
|
+
AUTH_PAYLOAD_SECRET,
|
|
157
|
+
AUTH_SIGN_SECRET,
|
|
158
|
+
accessToken,
|
|
159
|
+
}) => {
|
|
160
|
+
let payloadDecrypted: string;
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const [header, payload, signature] = accessToken.split('.');
|
|
164
|
+
|
|
165
|
+
payloadDecrypted = await decryptPayload({
|
|
166
|
+
AUTH_PAYLOAD_SECRET,
|
|
167
|
+
token: `${header}.${payload}`,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const key = await signatureKey(AUTH_SIGN_SECRET);
|
|
171
|
+
const valid = await crypto.subtle.verify(
|
|
172
|
+
'HMAC',
|
|
173
|
+
key,
|
|
174
|
+
fromBase64(signature).buffer as ArrayBuffer,
|
|
175
|
+
encoder.encode(payloadDecrypted)
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
if (!valid) throw new Error('Invalid access token');
|
|
179
|
+
} catch (err) {
|
|
180
|
+
// qualquer falha na decriptação ou verificação -> token inválido
|
|
181
|
+
throw new Error('Invalid access token');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// verifica expiração **fora do try/catch**, para não ser capturada como token inválido
|
|
185
|
+
const { expiresAt } = JSON.parse(payloadDecrypted);
|
|
186
|
+
if (Date.now() > new Date(expiresAt).getTime()) {
|
|
187
|
+
throw new Error('Expired access token');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return payloadDecrypted;
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
sha256ToHex: async (data: string): Promise<string> => {
|
|
194
|
+
const buffer = await crypto.subtle.digest('SHA-256', encoder.encode(data));
|
|
195
|
+
return Array.from(new Uint8Array(buffer))
|
|
196
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
197
|
+
.join('');
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
encryptPayload,
|
|
201
|
+
decryptPayload,
|
|
202
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gera um código de validação.
|
|
3
|
+
*
|
|
4
|
+
* @param {number} [length=6] - Número de caracteres do código.
|
|
5
|
+
* @param {boolean} [alphanumeric=true] - Se `true`, gera código alfanumérico (letras e números).
|
|
6
|
+
* Se `false`, gera apenas dígitos numéricos.
|
|
7
|
+
* @returns {string} Código gerado.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* generateCode(); // "482901"
|
|
11
|
+
* generateCode(8); // "83910274"
|
|
12
|
+
* generateCode(8, true); // "A9F3D2K7"
|
|
13
|
+
*/
|
|
14
|
+
export const generateCode = (length = 6, alphanumeric = true): string => {
|
|
15
|
+
const digits = '0123456789';
|
|
16
|
+
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
17
|
+
const charset = alphanumeric ? digits + letters : digits;
|
|
18
|
+
|
|
19
|
+
let result = '';
|
|
20
|
+
for (let i = 0; i < length; i++) {
|
|
21
|
+
const randomIndex = Math.floor(Math.random() * charset.length);
|
|
22
|
+
result += charset[randomIndex];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return result;
|
|
26
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createValidator } from './helpers/validator';
|
|
2
2
|
import { htmlEntityDecode } from './helpers/sanitize';
|
|
3
|
+
import { generateCode } from './helpers/generateCode';
|
|
3
4
|
import { stringUtils } from './helpers/string';
|
|
5
|
+
import { cryptoUtils } from './helpers/crypto';
|
|
4
6
|
import * as slugUtils from './helpers/slug';
|
|
5
7
|
import * as numberUtils from './helpers/number';
|
|
6
8
|
|
|
@@ -10,4 +12,6 @@ export {
|
|
|
10
12
|
slugUtils,
|
|
11
13
|
numberUtils,
|
|
12
14
|
stringUtils,
|
|
15
|
+
cryptoUtils,
|
|
16
|
+
generateCode,
|
|
13
17
|
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { cryptoUtils } from '../../src/helpers/crypto';
|
|
4
|
+
|
|
5
|
+
const AUTH_SIGN_SECRET = '12345678901234567890123456789012'; // 32 chars
|
|
6
|
+
const AUTH_PAYLOAD_SECRET = 'abcdefghijklmnopqrstuvxz12345678'; // 32 chars
|
|
7
|
+
|
|
8
|
+
describe('cryptoUtils helper', () => {
|
|
9
|
+
it('uuidV4 deve gerar um UUID válido', () => {
|
|
10
|
+
const id = cryptoUtils.uuidV4();
|
|
11
|
+
assert.match(
|
|
12
|
+
id,
|
|
13
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('hash e isMatch devem funcionar corretamente', async () => {
|
|
18
|
+
const password = 'mypassword';
|
|
19
|
+
const salt = 1234;
|
|
20
|
+
const hashed = await cryptoUtils.hash(password, salt);
|
|
21
|
+
|
|
22
|
+
assert.equal(await cryptoUtils.isMatch(password, hashed, salt), true);
|
|
23
|
+
assert.equal(
|
|
24
|
+
await cryptoUtils.isMatch('wrongpassword', hashed, salt),
|
|
25
|
+
false
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('encryptPayload e decryptPayload devem ser inversos', async () => {
|
|
30
|
+
const payload = JSON.stringify({ foo: 'bar' });
|
|
31
|
+
|
|
32
|
+
const encrypted = await cryptoUtils.encryptPayload({
|
|
33
|
+
AUTH_PAYLOAD_SECRET,
|
|
34
|
+
payload,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const decrypted = await cryptoUtils.decryptPayload({
|
|
38
|
+
AUTH_PAYLOAD_SECRET,
|
|
39
|
+
token: encrypted,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
assert.equal(decrypted, payload);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('sha256ToHex deve gerar hash esperado', async () => {
|
|
46
|
+
const data = 'hello';
|
|
47
|
+
const hash = await cryptoUtils.sha256ToHex(data);
|
|
48
|
+
|
|
49
|
+
// hash SHA-256 conhecido de "hello"
|
|
50
|
+
assert.equal(
|
|
51
|
+
hash,
|
|
52
|
+
'2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824'
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('generateAccessToken e validateAccessToken devem funcionar', async () => {
|
|
57
|
+
const payload = JSON.stringify({
|
|
58
|
+
userId: 42,
|
|
59
|
+
expiresAt: new Date(Date.now() + 1000 * 60).toISOString(), // +1 min
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const token = await cryptoUtils.generateAccessToken({
|
|
63
|
+
AUTH_SIGN_SECRET,
|
|
64
|
+
AUTH_PAYLOAD_SECRET,
|
|
65
|
+
payload,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const decrypted = await cryptoUtils.validateAccessToken({
|
|
69
|
+
AUTH_SIGN_SECRET,
|
|
70
|
+
AUTH_PAYLOAD_SECRET,
|
|
71
|
+
accessToken: token,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
assert.equal(decrypted, payload);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('validateAccessToken deve falhar com token expirado', async () => {
|
|
78
|
+
const payload = JSON.stringify({
|
|
79
|
+
userId: 42,
|
|
80
|
+
expiresAt: new Date(Date.now() - 1000).toISOString(), // já expirado
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const token = await cryptoUtils.generateAccessToken({
|
|
84
|
+
AUTH_SIGN_SECRET,
|
|
85
|
+
AUTH_PAYLOAD_SECRET,
|
|
86
|
+
payload,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await assert.rejects(
|
|
90
|
+
() =>
|
|
91
|
+
cryptoUtils.validateAccessToken({
|
|
92
|
+
AUTH_SIGN_SECRET,
|
|
93
|
+
AUTH_PAYLOAD_SECRET,
|
|
94
|
+
accessToken: token,
|
|
95
|
+
}),
|
|
96
|
+
/Expired access token/
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('validateAccessToken deve falhar com assinatura inválida', async () => {
|
|
101
|
+
const payload = JSON.stringify({
|
|
102
|
+
userId: 42,
|
|
103
|
+
expiresAt: new Date(Date.now() + 1000 * 60).toISOString(),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const token = await cryptoUtils.generateAccessToken({
|
|
107
|
+
AUTH_SIGN_SECRET,
|
|
108
|
+
AUTH_PAYLOAD_SECRET,
|
|
109
|
+
payload,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// altera assinatura (última parte do token)
|
|
113
|
+
const [header, body, signature] = token.split('.');
|
|
114
|
+
const tamperedToken = `${header}.${body}.${signature.slice(1)}A`;
|
|
115
|
+
|
|
116
|
+
await assert.rejects(
|
|
117
|
+
() =>
|
|
118
|
+
cryptoUtils.validateAccessToken({
|
|
119
|
+
AUTH_SIGN_SECRET,
|
|
120
|
+
AUTH_PAYLOAD_SECRET,
|
|
121
|
+
accessToken: tamperedToken,
|
|
122
|
+
}),
|
|
123
|
+
/Invalid access token/
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
});
|