samlesa 2.12.3 → 2.12.5
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.
Potentially problematic release.
This version of samlesa might be problematic. Click here for more details.
- package/build/.idea/workspace.xml +13 -1
- package/build/index.js +54 -64
- package/build/index.js.map +1 -1
- package/build/src/api.js +24 -23
- package/build/src/api.js.map +1 -1
- package/build/src/binding-post.js +358 -368
- package/build/src/binding-post.js.map +1 -1
- package/build/src/binding-redirect.js +333 -332
- package/build/src/binding-redirect.js.map +1 -1
- package/build/src/binding-simplesign.js +222 -232
- package/build/src/binding-simplesign.js.map +1 -1
- package/build/src/entity-idp.js +130 -130
- package/build/src/entity-idp.js.map +1 -1
- package/build/src/entity-sp.js +96 -96
- package/build/src/entity-sp.js.map +1 -1
- package/build/src/entity.js +225 -235
- package/build/src/entity.js.map +1 -1
- package/build/src/extractor.js +385 -369
- package/build/src/extractor.js.map +1 -1
- package/build/src/flow.js +320 -319
- package/build/src/flow.js.map +1 -1
- package/build/src/libsaml.js +665 -641
- package/build/src/libsaml.js.map +1 -1
- package/build/src/metadata-idp.js +127 -127
- package/build/src/metadata-idp.js.map +1 -1
- package/build/src/metadata-sp.js +231 -231
- package/build/src/metadata-sp.js.map +1 -1
- package/build/src/metadata.js +166 -176
- package/build/src/metadata.js.map +1 -1
- package/build/src/types.js +11 -11
- package/build/src/urn.js +212 -212
- package/build/src/urn.js.map +1 -1
- package/build/src/utility.js +292 -248
- package/build/src/utility.js.map +1 -1
- package/build/src/validator.js +27 -26
- package/build/src/validator.js.map +1 -1
- package/package.json +8 -10
- package/src/api.ts +1 -1
- package/src/binding-redirect.ts +83 -64
- package/src/extractor.ts +23 -5
- package/src/libsaml.ts +95 -62
- package/src/utility.ts +147 -76
- package/types/index.d.ts +10 -10
- package/types/src/api.d.ts +13 -13
- package/types/src/binding-post.d.ts +46 -46
- package/types/src/binding-redirect.d.ts +52 -52
- package/types/src/binding-simplesign.d.ts +39 -39
- package/types/src/entity-idp.d.ts +42 -42
- package/types/src/entity-sp.d.ts +36 -36
- package/types/src/entity.d.ts +101 -99
- package/types/src/extractor.d.ts +25 -25
- package/types/src/flow.d.ts +6 -6
- package/types/src/libsaml.d.ts +200 -210
- package/types/src/metadata-idp.d.ts +24 -24
- package/types/src/metadata-sp.d.ts +36 -36
- package/types/src/metadata.d.ts +59 -57
- package/types/src/types.d.ts +129 -127
- package/types/src/urn.d.ts +194 -194
- package/types/src/utility.d.ts +134 -134
- package/types/src/validator.d.ts +3 -3
package/src/libsaml.ts
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
* @author tngan
|
|
4
4
|
* @desc A simple library including some common functions
|
|
5
5
|
*/
|
|
6
|
-
|
|
6
|
+
import { createSign, createPrivateKey,createVerify } from 'node:crypto';
|
|
7
7
|
import utility, { flattenDeep, isString } from './utility.js';
|
|
8
8
|
import { algorithms, wording, namespace } from './urn.js';
|
|
9
9
|
import { select } from 'xpath';
|
|
10
10
|
import { MetadataInterface } from './metadata.js';
|
|
11
|
-
|
|
11
|
+
|
|
12
12
|
import { SignedXml } from 'xml-crypto';
|
|
13
13
|
import * as xmlenc from 'xml-encryption';
|
|
14
14
|
import { extract } from './extractor.js';
|
|
@@ -22,7 +22,22 @@ const signatureAlgorithms = algorithms.signature;
|
|
|
22
22
|
const digestAlgorithms = algorithms.digest;
|
|
23
23
|
const certUse = wording.certUse;
|
|
24
24
|
const urlParams = wording.urlParams;
|
|
25
|
+
/**
|
|
26
|
+
* 算法名称映射表 (兼容 X.509 和 SAML 规范)
|
|
27
|
+
*/
|
|
28
|
+
function mapSignAlgorithm(algorithm: string): string {
|
|
29
|
+
const algorithmMap = {
|
|
30
|
+
'rsa-sha1': 'RSA-SHA1',
|
|
31
|
+
'rsa-sha256': 'RSA-SHA256',
|
|
32
|
+
'rsa-sha384': 'RSA-SHA384',
|
|
33
|
+
'rsa-sha512': 'RSA-SHA512',
|
|
34
|
+
'ecdsa-sha256': 'ECDSA-SHA256',
|
|
35
|
+
'ecdsa-sha384': 'ECDSA-SHA384',
|
|
36
|
+
'ecdsa-sha512': 'ECDSA-SHA512'
|
|
37
|
+
};
|
|
25
38
|
|
|
39
|
+
return algorithmMap[algorithm.toLowerCase()] || algorithm;
|
|
40
|
+
}
|
|
26
41
|
export interface SignatureConstructor {
|
|
27
42
|
rawSamlMessage: string;
|
|
28
43
|
referenceTagXPath?: string;
|
|
@@ -139,6 +154,11 @@ const libSaml = () => {
|
|
|
139
154
|
'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256': 'pkcs1-sha256',
|
|
140
155
|
'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512': 'pkcs1-sha512',
|
|
141
156
|
};
|
|
157
|
+
const nrsaAliasMappingForNode = {
|
|
158
|
+
'http://www.w3.org/2000/09/xmldsig#rsa-sha1': 'RSA-SHA1',
|
|
159
|
+
'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256': 'RSA-SHA256',
|
|
160
|
+
'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512': 'RSA-SHA512',
|
|
161
|
+
};
|
|
142
162
|
/**
|
|
143
163
|
* @desc Default login request template
|
|
144
164
|
* @type {LoginRequestTemplate}
|
|
@@ -189,20 +209,15 @@ const libSaml = () => {
|
|
|
189
209
|
const defaultLogoutResponseTemplate = {
|
|
190
210
|
context: '<samlp:LogoutResponse xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="{ID}" Version="2.0" IssueInstant="{IssueInstant}" Destination="{Destination}" InResponseTo="{InResponseTo}"><saml:Issuer>{Issuer}</saml:Issuer><samlp:Status><samlp:StatusCode Value="{StatusCode}"/></samlp:Status></samlp:LogoutResponse>',
|
|
191
211
|
};
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
* @desc Get the signing scheme alias by signature algorithms, used by the node-rsa module
|
|
195
|
-
* @param {string} sigAlg signature algorithm
|
|
196
|
-
* @return {string/null} signing algorithm short-hand for the module node-rsa
|
|
197
|
-
*/
|
|
198
|
-
function getSigningScheme(sigAlg?: string): SigningSchemeHash {
|
|
212
|
+
|
|
213
|
+
function getSigningSchemeForNode(sigAlg?: string){
|
|
199
214
|
if (sigAlg) {
|
|
200
|
-
const algAlias =
|
|
215
|
+
const algAlias = nrsaAliasMappingForNode[sigAlg];
|
|
201
216
|
if (!(algAlias === undefined)) {
|
|
202
217
|
return algAlias;
|
|
203
218
|
}
|
|
204
219
|
}
|
|
205
|
-
return
|
|
220
|
+
return nrsaAliasMappingForNode[signatureAlgorithms.RSA_SHA1];
|
|
206
221
|
}
|
|
207
222
|
/**
|
|
208
223
|
* @private
|
|
@@ -376,7 +391,7 @@ const libSaml = () => {
|
|
|
376
391
|
*/
|
|
377
392
|
verifySignature(xml: string, opts: SignatureVerifierOptions) {
|
|
378
393
|
const { dom } = getContext();
|
|
379
|
-
const doc = dom.parseFromString(xml);
|
|
394
|
+
const doc = dom.parseFromString(xml,'application/xml') as unknown as Node;
|
|
380
395
|
|
|
381
396
|
const docParser = new DOMParser();
|
|
382
397
|
// In order to avoid the wrapping attack, we have changed to use absolute xpath instead of naively fetching the signature element
|
|
@@ -397,6 +412,7 @@ const libSaml = () => {
|
|
|
397
412
|
selection = selection.concat(assertionSignatureNode);
|
|
398
413
|
|
|
399
414
|
// try to catch potential wrapping attack
|
|
415
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
400
416
|
if (wrappingElementNode.length !== 0) {
|
|
401
417
|
throw new Error('ERR_POTENTIAL_WRAPPING_ATTACK');
|
|
402
418
|
}
|
|
@@ -476,14 +492,14 @@ const libSaml = () => {
|
|
|
476
492
|
// attempt is made to get the signed Reference as a string();
|
|
477
493
|
// note, we don't have access to the actual signedReferences API unfortunately
|
|
478
494
|
// mainly a sanity check here for SAML. (Although ours would still be secure, if multiple references are used)
|
|
479
|
-
if (!(sig.
|
|
495
|
+
if (!(sig.getSignedReferences().length >= 1)) {
|
|
480
496
|
throw new Error('NO_SIGNATURE_REFERENCES')
|
|
481
497
|
}
|
|
482
498
|
const signedVerifiedXML = sig.getSignedReferences()[0];
|
|
483
|
-
const rootNode = docParser.parseFromString(signedVerifiedXML, 'text/xml').documentElement;
|
|
499
|
+
const rootNode = docParser.parseFromString(signedVerifiedXML, 'text/xml').documentElement as unknown as Element;
|
|
484
500
|
// process the verified signature:
|
|
485
501
|
// case 1, rootSignedDoc is a response:
|
|
486
|
-
if (rootNode
|
|
502
|
+
if (rootNode?.localName === 'Response') {
|
|
487
503
|
|
|
488
504
|
// try getting the Xml from the first assertion
|
|
489
505
|
const EncryptedAssertions = select(
|
|
@@ -496,17 +512,18 @@ const libSaml = () => {
|
|
|
496
512
|
);
|
|
497
513
|
|
|
498
514
|
// now we can process the assertion as an assertion
|
|
515
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
499
516
|
if (EncryptedAssertions.length === 1) {
|
|
500
|
-
|
|
517
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
501
518
|
return [true, EncryptedAssertions[0].toString()];
|
|
502
519
|
}
|
|
503
|
-
|
|
520
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
504
521
|
if (assertions.length === 1) {
|
|
505
|
-
|
|
522
|
+
// @ts-expect-error misssing Node properties are not needed
|
|
506
523
|
return [true, assertions[0].toString()];
|
|
507
524
|
}
|
|
508
525
|
|
|
509
|
-
} else if (rootNode
|
|
526
|
+
} else if (rootNode?.localName === 'Assertion') {
|
|
510
527
|
return [true, rootNode.toString()];
|
|
511
528
|
} else {
|
|
512
529
|
return [true, null]; // signature is valid. But there is no assertion node here. It could be metadata node, hence return null
|
|
@@ -587,42 +604,51 @@ const libSaml = () => {
|
|
|
587
604
|
}],
|
|
588
605
|
};
|
|
589
606
|
},
|
|
607
|
+
|
|
590
608
|
/**
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
609
|
+
* SAML 消息签名 (符合 SAML V2.0 绑定规范)
|
|
610
|
+
* @param octetString - 要签名的原始数据 (OCTET STRING)
|
|
611
|
+
* @param key - PEM 格式私钥
|
|
612
|
+
* @param passphrase - 私钥密码 (如果有加密)
|
|
613
|
+
* @param isBase64 - 是否返回 base64 编码 (默认 true)
|
|
614
|
+
* @param signingAlgorithm - 签名算法 (默认 'rsa-sha256')
|
|
615
|
+
* @returns 消息签名
|
|
616
|
+
*/
|
|
617
|
+
|
|
598
618
|
constructMessageSignature(
|
|
599
|
-
octetString: string,
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
//
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
619
|
+
octetString: string | Buffer,
|
|
620
|
+
key: string | Buffer,
|
|
621
|
+
passphrase?: string,
|
|
622
|
+
isBase64: boolean = true,
|
|
623
|
+
signingAlgorithm: string = nrsaAliasMappingForNode[signatureAlgorithms.RSA_SHA1]
|
|
624
|
+
): string | Buffer {
|
|
625
|
+
try {
|
|
626
|
+
// 1. 标准化输入数据
|
|
627
|
+
const inputData = Buffer.isBuffer(octetString)
|
|
628
|
+
? octetString
|
|
629
|
+
: Buffer.from(octetString, 'utf8');
|
|
630
|
+
// 2. 创建签名器并设置算
|
|
631
|
+
const signingAlgorithmValue = getSigningSchemeForNode(signingAlgorithm)
|
|
632
|
+
const signer = createSign(signingAlgorithmValue)
|
|
633
|
+
|
|
634
|
+
// 3. 加载私钥
|
|
635
|
+
const privateKey = createPrivateKey({
|
|
636
|
+
key: key,
|
|
637
|
+
format: 'pem',
|
|
638
|
+
passphrase: passphrase,
|
|
639
|
+
encoding: 'utf8'
|
|
640
|
+
});
|
|
641
|
+
signer.write(octetString);
|
|
642
|
+
signer.end();
|
|
643
|
+
const signature = signer.sign(privateKey, 'base64');
|
|
644
|
+
console.log(signature.toString());
|
|
645
|
+
console.log('dayingyixia')
|
|
646
|
+
// 5. 处理编码输出
|
|
647
|
+
return isBase64 ? signature.toString() : signature;
|
|
648
|
+
} catch (error) {
|
|
649
|
+
throw new Error(`SAML 签名失败: ${error.message}`);
|
|
650
|
+
}
|
|
651
|
+
},
|
|
626
652
|
verifyMessageSignature(
|
|
627
653
|
metadata,
|
|
628
654
|
octetString: string,
|
|
@@ -630,10 +656,17 @@ const libSaml = () => {
|
|
|
630
656
|
verifyAlgorithm?: string
|
|
631
657
|
) {
|
|
632
658
|
const signCert = metadata.getX509Certificate(certUse.signing);
|
|
633
|
-
const signingScheme =
|
|
634
|
-
const
|
|
635
|
-
|
|
659
|
+
const signingScheme = getSigningSchemeForNode(verifyAlgorithm);
|
|
660
|
+
const verifier = createVerify(signingScheme);
|
|
661
|
+
verifier.update(octetString);
|
|
662
|
+
const isValid = verifier.verify(utility.getPublicKeyPemFromCertificate(signCert), Buffer.isBuffer(signature) ? signature : Buffer.from(signature, 'base64'));
|
|
663
|
+
console.log(isValid);
|
|
664
|
+
console.log('验证结果-------------')
|
|
665
|
+
return isValid
|
|
666
|
+
|
|
636
667
|
},
|
|
668
|
+
|
|
669
|
+
|
|
637
670
|
/**
|
|
638
671
|
* @desc Get the public key in string format
|
|
639
672
|
* @param {string} x509Certificate certificate
|
|
@@ -668,7 +701,7 @@ const libSaml = () => {
|
|
|
668
701
|
const sourceEntitySetting = sourceEntity.entitySetting;
|
|
669
702
|
const targetEntityMetadata = targetEntity.entityMeta;
|
|
670
703
|
const { dom } = getContext();
|
|
671
|
-
const doc = dom.parseFromString(xml);
|
|
704
|
+
const doc = dom.parseFromString(xml,'application/xml') as unknown as Node;
|
|
672
705
|
const assertions = select("//*[local-name(.)='Assertion']", doc) as Node[];
|
|
673
706
|
if (!Array.isArray(assertions) || assertions.length === 0) {
|
|
674
707
|
throw new Error('ERR_NO_ASSERTION');
|
|
@@ -701,8 +734,8 @@ const libSaml = () => {
|
|
|
701
734
|
return reject(new Error('ERR_UNDEFINED_ENCRYPTED_ASSERTION'));
|
|
702
735
|
}
|
|
703
736
|
const { encryptedAssertion: encAssertionPrefix } = sourceEntitySetting.tagPrefix;
|
|
704
|
-
const encryptAssertionDoc = dom.parseFromString(`<${encAssertionPrefix}:EncryptedAssertion xmlns:${encAssertionPrefix}="${namespace.names.assertion}">${res}</${encAssertionPrefix}:EncryptedAssertion
|
|
705
|
-
doc
|
|
737
|
+
const encryptAssertionDoc = dom.parseFromString(`<${encAssertionPrefix}:EncryptedAssertion xmlns:${encAssertionPrefix}="${namespace.names.assertion}">${res}</${encAssertionPrefix}:EncryptedAssertion>`,'application/xml');
|
|
738
|
+
(doc as Document)?.replaceChild(encryptAssertionDoc.documentElement as unknown as Node, rawAssertionNode) ;
|
|
706
739
|
return resolve(utility.base64Encode(doc.toString()));
|
|
707
740
|
});
|
|
708
741
|
} else {
|
|
@@ -727,7 +760,7 @@ const libSaml = () => {
|
|
|
727
760
|
// Perform encryption depends on the setting of where the message is sent, default is false
|
|
728
761
|
const hereSetting = here.entitySetting;
|
|
729
762
|
const { dom } = getContext();
|
|
730
|
-
const doc = dom.parseFromString(entireXML);
|
|
763
|
+
const doc = dom.parseFromString(entireXML,'application/xml') as unknown as Node;
|
|
731
764
|
const encryptedAssertions = select("/*[contains(local-name(), 'Response')]/*[local-name(.)='EncryptedAssertion']", doc) as Node[];
|
|
732
765
|
if (!Array.isArray(encryptedAssertions) || encryptedAssertions.length === 0) {
|
|
733
766
|
throw new Error('ERR_UNDEFINED_ENCRYPTED_ASSERTION');
|
|
@@ -746,8 +779,8 @@ const libSaml = () => {
|
|
|
746
779
|
if (!res) {
|
|
747
780
|
return reject(new Error('ERR_UNDEFINED_ENCRYPTED_ASSERTION'));
|
|
748
781
|
}
|
|
749
|
-
const rawAssertionDoc = dom.parseFromString(res);
|
|
750
|
-
doc
|
|
782
|
+
const rawAssertionDoc = dom.parseFromString(res,'application/xml') ;
|
|
783
|
+
(doc as Document)?.replaceChild(rawAssertionDoc?.documentElement as unknown as Node , encAssertionNode);
|
|
751
784
|
return resolve([doc.toString(), res]);
|
|
752
785
|
});
|
|
753
786
|
});
|
package/src/utility.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file utility.ts
|
|
3
|
-
* @author tngan
|
|
4
|
-
* @desc Library for some common functions (e.g. de/inflation, en/decoding)
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import {
|
|
2
|
+
* @file utility.ts
|
|
3
|
+
* @author tngan
|
|
4
|
+
* @desc Library for some common functions (e.g. de/inflation, en/decoding)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {X509Certificate,createPrivateKey } from 'node:crypto';
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import {inflate, deflate} from 'pako';
|
|
11
11
|
|
|
12
12
|
const BASE64_STR = 'base64';
|
|
13
13
|
|
|
@@ -36,6 +36,7 @@ export function zipObject(arr1: string[], arr2: any[], skipDuplicated = true) {
|
|
|
36
36
|
|
|
37
37
|
}, {});
|
|
38
38
|
}
|
|
39
|
+
|
|
39
40
|
/**
|
|
40
41
|
* @desc Alternative to lodash.flattenDeep
|
|
41
42
|
* @reference https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore#_flattendeep
|
|
@@ -43,9 +44,10 @@ export function zipObject(arr1: string[], arr2: any[], skipDuplicated = true) {
|
|
|
43
44
|
*/
|
|
44
45
|
export function flattenDeep(input: any[]) {
|
|
45
46
|
return Array.isArray(input)
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
? input.reduce((a, b) => a.concat(flattenDeep(b)), [])
|
|
48
|
+
: [input];
|
|
48
49
|
}
|
|
50
|
+
|
|
49
51
|
/**
|
|
50
52
|
* @desc Alternative to lodash.last
|
|
51
53
|
* @reference https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore#_last
|
|
@@ -54,6 +56,7 @@ export function flattenDeep(input: any[]) {
|
|
|
54
56
|
export function last(input: any[]) {
|
|
55
57
|
return input.slice(-1)[0];
|
|
56
58
|
}
|
|
59
|
+
|
|
57
60
|
/**
|
|
58
61
|
* @desc Alternative to lodash.uniq
|
|
59
62
|
* @reference https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore#_uniq
|
|
@@ -61,8 +64,9 @@ export function last(input: any[]) {
|
|
|
61
64
|
*/
|
|
62
65
|
export function uniq(input: string[]) {
|
|
63
66
|
const set = new Set(input);
|
|
64
|
-
return [...
|
|
67
|
+
return [...set];
|
|
65
68
|
}
|
|
69
|
+
|
|
66
70
|
/**
|
|
67
71
|
* @desc Alternative to lodash.get
|
|
68
72
|
* @reference https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore#_get
|
|
@@ -72,8 +76,9 @@ export function uniq(input: string[]) {
|
|
|
72
76
|
*/
|
|
73
77
|
export function get(obj, path, defaultValue) {
|
|
74
78
|
return path.split('.')
|
|
75
|
-
|
|
79
|
+
.reduce((a, c) => (a && a[c] ? a[c] : (defaultValue || null)), obj);
|
|
76
80
|
}
|
|
81
|
+
|
|
77
82
|
/**
|
|
78
83
|
* @desc Check if the input is string
|
|
79
84
|
* @param {any} input
|
|
@@ -81,107 +86,123 @@ export function get(obj, path, defaultValue) {
|
|
|
81
86
|
export function isString(input: any) {
|
|
82
87
|
return typeof input === 'string';
|
|
83
88
|
}
|
|
89
|
+
|
|
84
90
|
/**
|
|
85
|
-
* @desc Encode string with base64 format
|
|
86
|
-
* @param {string} message plain-text message
|
|
87
|
-
* @return {string} base64 encoded string
|
|
88
|
-
*/
|
|
91
|
+
* @desc Encode string with base64 format
|
|
92
|
+
* @param {string} message plain-text message
|
|
93
|
+
* @return {string} base64 encoded string
|
|
94
|
+
*/
|
|
89
95
|
function base64Encode(message: string | number[]) {
|
|
90
96
|
return Buffer.from(message as string).toString(BASE64_STR);
|
|
91
97
|
}
|
|
98
|
+
|
|
92
99
|
/**
|
|
93
|
-
* @desc Decode string from base64 format
|
|
94
|
-
* @param {string} base64Message encoded string
|
|
95
|
-
* @param {boolean} isBytes determine the return value type (True: bytes False: string)
|
|
96
|
-
* @return {bytes/string} decoded bytes/string depends on isBytes, default is {string}
|
|
97
|
-
*/
|
|
100
|
+
* @desc Decode string from base64 format
|
|
101
|
+
* @param {string} base64Message encoded string
|
|
102
|
+
* @param {boolean} isBytes determine the return value type (True: bytes False: string)
|
|
103
|
+
* @return {bytes/string} decoded bytes/string depends on isBytes, default is {string}
|
|
104
|
+
*/
|
|
98
105
|
export function base64Decode(base64Message: string, isBytes?: boolean): string | Buffer {
|
|
99
106
|
const bytes = Buffer.from(base64Message, BASE64_STR);
|
|
100
107
|
return Boolean(isBytes) ? bytes : bytes.toString();
|
|
101
108
|
}
|
|
109
|
+
|
|
102
110
|
/**
|
|
103
|
-
* @desc Compress the string
|
|
104
|
-
* @param {string} message
|
|
105
|
-
* @return {string} compressed string
|
|
106
|
-
*/
|
|
111
|
+
* @desc Compress the string
|
|
112
|
+
* @param {string} message
|
|
113
|
+
* @return {string} compressed string
|
|
114
|
+
*/
|
|
107
115
|
function deflateString(message: string): number[] {
|
|
108
116
|
const input = Array.prototype.map.call(message, char => char.charCodeAt(0));
|
|
109
|
-
return Array.from(deflate(input, {
|
|
117
|
+
return Array.from(deflate(input, {raw: true}));
|
|
110
118
|
}
|
|
119
|
+
|
|
111
120
|
/**
|
|
112
|
-
* @desc Decompress the compressed string
|
|
113
|
-
* @param {string} compressedString
|
|
114
|
-
* @return {string} decompressed string
|
|
115
|
-
*/
|
|
121
|
+
* @desc Decompress the compressed string
|
|
122
|
+
* @param {string} compressedString
|
|
123
|
+
* @return {string} decompressed string
|
|
124
|
+
*/
|
|
116
125
|
export function inflateString(compressedString: string): string {
|
|
117
126
|
const inputBuffer = Buffer.from(compressedString, BASE64_STR);
|
|
118
127
|
const input = Array.prototype.map.call(inputBuffer.toString('binary'), char => char.charCodeAt(0));
|
|
119
|
-
return Array.from(inflate(input, {
|
|
128
|
+
return Array.from(inflate(input, {raw: true}))
|
|
120
129
|
.map((byte: number) => String.fromCharCode(byte))
|
|
121
130
|
.join('');
|
|
122
131
|
}
|
|
132
|
+
|
|
123
133
|
/**
|
|
124
|
-
* @desc Abstract the normalizeCerString and normalizePemString
|
|
125
|
-
* @param {buffer} File stream or string
|
|
126
|
-
* @param {string} String for header and tail
|
|
127
|
-
* @return {string} A formatted certificate string
|
|
128
|
-
*/
|
|
134
|
+
* @desc Abstract the normalizeCerString and normalizePemString
|
|
135
|
+
* @param {buffer} File stream or string
|
|
136
|
+
* @param {string} String for header and tail
|
|
137
|
+
* @return {string} A formatted certificate string
|
|
138
|
+
*/
|
|
129
139
|
function _normalizeCerString(bin: string | Buffer, format: string) {
|
|
130
140
|
return bin.toString().replace(/\n/g, '').replace(/\r/g, '').replace(`-----BEGIN ${format}-----`, '').replace(`-----END ${format}-----`, '').replace(/ /g, '').replace(/\t/g, '');
|
|
131
141
|
}
|
|
142
|
+
|
|
132
143
|
/**
|
|
133
|
-
* @desc Parse the .cer to string format without line break, header and footer
|
|
134
|
-
* @param {string} certString declares the certificate contents
|
|
135
|
-
* @return {string} certificiate in string format
|
|
136
|
-
*/
|
|
144
|
+
* @desc Parse the .cer to string format without line break, header and footer
|
|
145
|
+
* @param {string} certString declares the certificate contents
|
|
146
|
+
* @return {string} certificiate in string format
|
|
147
|
+
*/
|
|
137
148
|
function normalizeCerString(certString: string | Buffer) {
|
|
138
149
|
return _normalizeCerString(certString, 'CERTIFICATE');
|
|
139
150
|
}
|
|
151
|
+
|
|
140
152
|
/**
|
|
141
|
-
* @desc Normalize the string in .pem format without line break, header and footer
|
|
142
|
-
* @param {string} pemString
|
|
143
|
-
* @return {string} private key in string format
|
|
144
|
-
*/
|
|
153
|
+
* @desc Normalize the string in .pem format without line break, header and footer
|
|
154
|
+
* @param {string} pemString
|
|
155
|
+
* @return {string} private key in string format
|
|
156
|
+
*/
|
|
145
157
|
function normalizePemString(pemString: string | Buffer) {
|
|
146
158
|
return _normalizeCerString(pemString.toString(), 'RSA PRIVATE KEY');
|
|
147
159
|
}
|
|
160
|
+
|
|
148
161
|
/**
|
|
149
|
-
* @desc Return the complete URL
|
|
150
|
-
* @param {object} req HTTP request
|
|
151
|
-
* @return {string} URL
|
|
152
|
-
*/
|
|
162
|
+
* @desc Return the complete URL
|
|
163
|
+
* @param {object} req HTTP request
|
|
164
|
+
* @return {string} URL
|
|
165
|
+
*/
|
|
153
166
|
function getFullURL(req) {
|
|
154
167
|
return `${req.protocol}://${req.get('host')}${req.originalUrl}`;
|
|
155
168
|
}
|
|
169
|
+
|
|
156
170
|
/**
|
|
157
|
-
* @desc Parse input string, return default value if it is undefined
|
|
158
|
-
* @param {string/boolean}
|
|
159
|
-
* @return {boolean}
|
|
160
|
-
*/
|
|
171
|
+
* @desc Parse input string, return default value if it is undefined
|
|
172
|
+
* @param {string/boolean}
|
|
173
|
+
* @return {boolean}
|
|
174
|
+
*/
|
|
161
175
|
function parseString(str, defaultValue = '') {
|
|
162
176
|
return str || defaultValue;
|
|
163
177
|
}
|
|
178
|
+
|
|
164
179
|
/**
|
|
165
|
-
* @desc Override the object by another object (rtl)
|
|
166
|
-
* @param {object} default object
|
|
167
|
-
* @param {object} object applied to the default object
|
|
168
|
-
* @return {object} result object
|
|
169
|
-
*/
|
|
180
|
+
* @desc Override the object by another object (rtl)
|
|
181
|
+
* @param {object} default object
|
|
182
|
+
* @param {object} object applied to the default object
|
|
183
|
+
* @return {object} result object
|
|
184
|
+
*/
|
|
170
185
|
function applyDefault(obj1, obj2) {
|
|
171
186
|
return Object.assign({}, obj1, obj2);
|
|
172
187
|
}
|
|
188
|
+
|
|
173
189
|
/**
|
|
174
|
-
* @desc Get public key in pem format from the certificate included in the metadata
|
|
175
|
-
* @param {string} x509 certificate
|
|
176
|
-
* @return {string} public key fetched from the certificate
|
|
177
|
-
*/
|
|
190
|
+
* @desc Get public key in pem format from the certificate included in the metadata
|
|
191
|
+
* @param {string} x509 certificate
|
|
192
|
+
* @return {string} public key fetched from the certificate
|
|
193
|
+
*/
|
|
178
194
|
function getPublicKeyPemFromCertificate(x509CertificateString: string) {
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
195
|
+
const derBuffer = Buffer.from(x509CertificateString, 'base64');
|
|
196
|
+
// 解析 X.509 证书
|
|
197
|
+
const cert2 = new X509Certificate(derBuffer);
|
|
198
|
+
const publicKeyObject = cert2.publicKey
|
|
199
|
+
// 3. 导出为 PEM 格式
|
|
200
|
+
return publicKeyObject.export({
|
|
201
|
+
type: 'spki', // 使用 Subject Public Key Info 结构
|
|
202
|
+
format: 'pem' // 输出 PEM 格式
|
|
203
|
+
});
|
|
184
204
|
|
|
205
|
+
}
|
|
185
206
|
|
|
186
207
|
|
|
187
208
|
/*function getPublicKeyPemFromCertificate(x509Certificate: string): string {
|
|
@@ -197,25 +218,75 @@ function getPublicKeyPemFromCertificate(x509CertificateString: string) {
|
|
|
197
218
|
return cert.publicKey?.toString();
|
|
198
219
|
}*/
|
|
199
220
|
/**
|
|
200
|
-
* @desc Read private key from pem-formatted string
|
|
201
|
-
* @param {string | Buffer} keyString pem-formatted string
|
|
202
|
-
* @param {string} protected passphrase of the key
|
|
203
|
-
* @return {string} string in pem format
|
|
204
|
-
* If passphrase is used to protect the .pem content (recommend)
|
|
205
|
-
*/
|
|
206
|
-
|
|
207
|
-
|
|
221
|
+
* @desc Read private key from pem-formatted string
|
|
222
|
+
* @param {string | Buffer} keyString pem-formatted string
|
|
223
|
+
* @param {string} protected passphrase of the key
|
|
224
|
+
* @return {string} string in pem format
|
|
225
|
+
* If passphrase is used to protect the .pem content (recommend)
|
|
226
|
+
*/
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* PEM 头尾格式校验与修复
|
|
230
|
+
*/
|
|
231
|
+
function validatePEMHeaders(pem: string, keyType: string): string {
|
|
232
|
+
const expectedHeader = `-----BEGIN ${keyType}-----`;
|
|
233
|
+
const expectedFooter = `-----END ${keyType}-----`;
|
|
234
|
+
|
|
235
|
+
// 自动修复不规范的 PEM 头尾
|
|
236
|
+
return pem
|
|
237
|
+
.replace(/-{5}.*PRIVATE KEY-{5}/g, '') // 清除已有头尾
|
|
238
|
+
.replace(/(\r\n|\n|\r)/gm, '\n') // 统一换行符
|
|
239
|
+
.trim() + // 清理空白
|
|
240
|
+
`\n${expectedHeader}\n${pem}\n${expectedFooter}\n`;
|
|
208
241
|
}
|
|
242
|
+
export function readPrivateKey(
|
|
243
|
+
keyString: string | Buffer,
|
|
244
|
+
passphrase?: string,
|
|
245
|
+
isOutputString: boolean = true
|
|
246
|
+
): string | Buffer {
|
|
247
|
+
try {
|
|
248
|
+
// 统一转换为字符串格式处理
|
|
249
|
+
const pemKey = Buffer.isBuffer(keyString)
|
|
250
|
+
? keyString.toString('utf8')
|
|
251
|
+
: keyString;
|
|
252
|
+
|
|
253
|
+
// 创建私钥对象 (自动处理加密)
|
|
254
|
+
const keyObject = createPrivateKey({
|
|
255
|
+
key: pemKey,
|
|
256
|
+
format: 'pem',
|
|
257
|
+
passphrase: isString(passphrase) ? passphrase : undefined,
|
|
258
|
+
encoding: 'utf8'
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// 验证密钥类型为 RSA
|
|
262
|
+
if (keyObject.asymmetricKeyType !== 'rsa') {
|
|
263
|
+
throw new Error('仅支持 RSA 私钥类型');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 强制转换为 PKCS#1 格式
|
|
267
|
+
const exported = keyObject.export({
|
|
268
|
+
type: 'pkcs1', // 明确指定 RSA 传统格式
|
|
269
|
+
format: 'pem' // 输出为 PEM 格式
|
|
270
|
+
}) as string;
|
|
271
|
+
|
|
272
|
+
return isOutputString ? String(exported) : Buffer.from(exported, 'utf8');
|
|
273
|
+
} catch (error) {
|
|
274
|
+
throw new Error(`私钥读取失败: ${error.message}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
|
|
209
279
|
/**
|
|
210
|
-
* @desc Inline syntax sugar
|
|
211
|
-
*/
|
|
280
|
+
* @desc Inline syntax sugar
|
|
281
|
+
*/
|
|
212
282
|
function convertToString(input, isOutputString) {
|
|
213
283
|
return Boolean(isOutputString) ? String(input) : input;
|
|
214
284
|
}
|
|
285
|
+
|
|
215
286
|
/**
|
|
216
287
|
* @desc Check if the input is an array with non-zero size
|
|
217
288
|
*/
|
|
218
|
-
export function isNonEmptyArray(a:any) {
|
|
289
|
+
export function isNonEmptyArray(a: any) {
|
|
219
290
|
return Array.isArray(a) && a.length > 0;
|
|
220
291
|
}
|
|
221
292
|
|
package/types/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import IdentityProvider, { IdentityProvider as IdentityProviderInstance } from './src/entity-idp.js';
|
|
2
|
-
import ServiceProvider, { ServiceProvider as ServiceProviderInstance } from './src/entity-sp.js';
|
|
3
|
-
export { default as IdPMetadata } from './src/metadata-idp.js';
|
|
4
|
-
export { default as SPMetadata } from './src/metadata-sp.js';
|
|
5
|
-
export { default as Utility } from './src/utility.js';
|
|
6
|
-
export { default as SamlLib } from './src/libsaml.js';
|
|
7
|
-
import * as Constants from './src/urn.js';
|
|
8
|
-
import * as Extractor from './src/extractor.js';
|
|
9
|
-
import { setSchemaValidator, setDOMParserOptions } from './src/api.js';
|
|
10
|
-
export { Constants, Extractor, IdentityProvider, IdentityProviderInstance, ServiceProvider, ServiceProviderInstance, setSchemaValidator, setDOMParserOptions };
|
|
1
|
+
import IdentityProvider, { IdentityProvider as IdentityProviderInstance } from './src/entity-idp.js';
|
|
2
|
+
import ServiceProvider, { ServiceProvider as ServiceProviderInstance } from './src/entity-sp.js';
|
|
3
|
+
export { default as IdPMetadata } from './src/metadata-idp.js';
|
|
4
|
+
export { default as SPMetadata } from './src/metadata-sp.js';
|
|
5
|
+
export { default as Utility } from './src/utility.js';
|
|
6
|
+
export { default as SamlLib } from './src/libsaml.js';
|
|
7
|
+
import * as Constants from './src/urn.js';
|
|
8
|
+
import * as Extractor from './src/extractor.js';
|
|
9
|
+
import { setSchemaValidator, setDOMParserOptions } from './src/api.js';
|
|
10
|
+
export { Constants, Extractor, IdentityProvider, IdentityProviderInstance, ServiceProvider, ServiceProviderInstance, setSchemaValidator, setDOMParserOptions };
|
package/types/src/api.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { DOMParser as dom,
|
|
2
|
-
interface Context extends ValidatorContext, DOMParserContext {
|
|
3
|
-
}
|
|
4
|
-
interface ValidatorContext {
|
|
5
|
-
validate?: (xml: string) => Promise<any>;
|
|
6
|
-
}
|
|
7
|
-
interface DOMParserContext {
|
|
8
|
-
dom: dom;
|
|
9
|
-
}
|
|
10
|
-
export declare function getContext(): Context;
|
|
11
|
-
export declare function setSchemaValidator(params: ValidatorContext): void;
|
|
12
|
-
export declare function setDOMParserOptions(options?: DOMParserOptions): void;
|
|
13
|
-
export {};
|
|
1
|
+
import { DOMParser as dom, DOMParserOptions } from '@xmldom/xmldom';
|
|
2
|
+
interface Context extends ValidatorContext, DOMParserContext {
|
|
3
|
+
}
|
|
4
|
+
interface ValidatorContext {
|
|
5
|
+
validate?: (xml: string) => Promise<any>;
|
|
6
|
+
}
|
|
7
|
+
interface DOMParserContext {
|
|
8
|
+
dom: dom;
|
|
9
|
+
}
|
|
10
|
+
export declare function getContext(): Context;
|
|
11
|
+
export declare function setSchemaValidator(params: ValidatorContext): void;
|
|
12
|
+
export declare function setDOMParserOptions(options?: DOMParserOptions): void;
|
|
13
|
+
export {};
|