cassproject 5.0.20 → 5.0.23
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/package.json +49 -52
- package/src/com/eduworks/ec/array/EcArray.js +1 -1
- package/src/com/eduworks/ec/array/EcAsyncHelper.js +1 -1
- package/src/com/eduworks/ec/array/EcObject.js +1 -1
- package/src/com/eduworks/ec/crypto/EcAes.js +1 -1
- package/src/com/eduworks/ec/crypto/EcAesCtrAsync.js +20 -22
- package/src/com/eduworks/ec/crypto/EcCrypto.js +8 -4
- package/src/com/eduworks/ec/crypto/EcPk.js +7 -7
- package/src/com/eduworks/ec/crypto/EcPpk.js +7 -7
- package/src/com/eduworks/ec/crypto/EcRsaOaepAsync.js +20 -19
- package/src/com/eduworks/ec/crypto/EcRsaOaepAsyncWorker.js +7 -6
- package/src/com/eduworks/ec/crypto/forgeAsync.js +1 -9
- package/src/com/eduworks/ec/crypto/forgeAsyncNode.js +8 -8
- package/src/com/eduworks/ec/graph/EcDirectedGraph.js +1 -2
- package/src/com/eduworks/ec/graph/EcFrameworkGraph.js +18 -24
- package/src/com/eduworks/ec/promises/helpers.js +1 -3
- package/src/com/eduworks/ec/remote/EcRemote.js +9 -9
- package/src/com/eduworks/ec/task/EcAsyncTask.js +5 -20
- package/src/com/eduworks/ec/task/EcAsyncTaskManager.js +3 -4
- package/src/com/eduworks/ec/task/EcAsyncTaskWorker.js +9 -11
- package/src/com/eduworks/schema/cfd/competency/CfdFramework.js +36 -37
- package/src/com/eduworks/schema/ebac/EbacContact.js +3 -9
- package/src/com/eduworks/schema/ebac/EbacContactGrant.js +3 -9
- package/src/com/eduworks/schema/ebac/EbacCredential.js +3 -9
- package/src/com/eduworks/schema/ebac/EbacCredentialCommit.js +3 -9
- package/src/com/eduworks/schema/ebac/EbacCredentialRequest.js +3 -9
- package/src/com/eduworks/schema/ebac/EbacCredentials.js +3 -9
- package/src/com/eduworks/schema/ebac/EbacEncryptedSecret.js +3 -9
- package/src/com/eduworks/schema/ebac/EbacEncryptedValue.js +5 -12
- package/src/com/eduworks/schema/ebac/EbacSignature.js +1 -6
- package/src/index/env.js +6 -3
- package/src/index/polyfills.js +13 -27
- package/src/org/angles/schema/angles/VideoStory.js +1 -1
- package/src/org/cass/competency/EcCompetency.js +1 -2
- package/src/org/cass/competency/EcDirectory.js +1 -2
- package/src/org/cass/competency/EcFramework.js +9 -12
- package/src/org/cass/exporter/CSVExport.js +12 -13
- package/src/org/cass/importer/ASNImport.js +3 -6
- package/src/org/cass/importer/CSVImport.js +9 -17
- package/src/org/cass/importer/CTDLASNCSVConceptImport.js +25 -121
- package/src/org/cass/importer/CTDLASNCSVImport.js +88 -133
- package/src/org/cass/importer/FrameworkImport.js +16 -30
- package/src/org/cass/importer/MedbiqImport.js +3 -4
- package/src/org/cass/importer/TabStructuredImport.js +6 -6
- package/src/org/cass/profile/EcAssertion.js +40 -51
- package/src/org/cassproject/ebac/identity/EcContactGrant.js +2 -2
- package/src/org/cassproject/ebac/identity/EcIdentityManager.js +56 -62
- package/src/org/cassproject/ebac/identity/EcRekeyRequest.js +3 -2
- package/src/org/cassproject/ebac/identity/remote/EcRemoteIdentityManager.js +51 -62
- package/src/org/cassproject/ebac/identity/remote/OAuth2FileBasedRemoteIdentityManager.js +56 -70
- package/src/org/cassproject/ebac/identity/remote/RemoteIdentityManagerInterface.js +3 -3
- package/src/org/cassproject/ebac/repository/EcEncryptedValue.js +51 -44
- package/src/org/cassproject/ebac/repository/EcRepository.js +228 -301
- package/src/org/cassproject/general/repository/GeneralFile.js +8 -9
- package/src/org/cassproject/schema/cass/competency/Competency.js +10 -11
- package/src/org/cassproject/schema/cass/competency/Framework.js +10 -11
- package/src/org/cassproject/schema/cass/competency/Level.js +10 -11
- package/src/org/cassproject/schema/cass/competency/Relation.js +10 -11
- package/src/org/cassproject/schema/cass/competency/RollupRule.js +7 -7
- package/src/org/cassproject/schema/cass/profile/Assertion.js +20 -27
- package/src/org/cassproject/schema/cass/profile/AssertionEnvelope.js +3 -5
- package/src/org/cassproject/schema/general/EcRemoteLinkedData.js +41 -44
- package/src/org/json/ld/EcLinkedData.js +10 -12
- package/src/org/schema/EcOrganization.js +10 -10
- package/src/org/schema/EcQuestion.js +8 -8
- package/src/org/w3/skos/Concept.js +6 -6
- package/src/org/w3/skos/ConceptScheme.js +6 -6
- package/src/org/w3/skos/EcConcept.js +1 -2
- package/src/org/w3/skos/EcConceptScheme.js +1 -2
- package/src/test/0.start.test.js +1 -1
- package/src/test/0.testLibraries.test.js +172 -0
- package/src/test/1.ASNImport.test.js +122 -0
- package/src/test/1.CSVExport.test.js +57 -0
- package/src/test/1.CSVImport.test.js +185 -0
- package/src/test/1.CTDLASNCSVConceptImport.test.js +348 -0
- package/src/test/1.CTDLASNCSVImport.import.test.js +497 -0
- package/src/test/1.Ebac.schema.test.js +107 -0
- package/src/test/1.EcAssertion.decrypt.test.js +98 -0
- package/src/test/1.EcAssertion.pinned.test.js +160 -0
- package/src/test/1.EcCrypto.formats.test.js +293 -0
- package/src/test/1.EcCrypto.generateUUID.test.js +47 -0
- package/src/test/1.EcEncryptedValue.helpers.test.js +236 -0
- package/src/test/1.EcHelpers.misc.test.js +259 -0
- package/src/test/1.EcIdentityManager.keys.test.js +439 -0
- package/src/test/1.EcOrganization.rekey.test.js +68 -0
- package/src/test/1.EcRemote.responses.test.js +155 -0
- package/src/test/1.EcRemoteIdentityManager.flows.test.js +326 -0
- package/src/test/1.EcRemoteLinkedData.pinned.test.js +473 -0
- package/src/test/1.EcRepository.l0.test.js +14 -5
- package/src/test/1.EcRepository.l1.test.js +16 -7
- package/src/test/1.EcRepository.l2.test.js +20 -11
- package/src/test/1.EcRepository.unit.test.js +961 -0
- package/src/test/1.EcRsaOaep.test.js +2 -12
- package/src/test/1.EcRsaOaepAsync.test.js +2 -12
- package/src/test/1.EcRsaOaepAsyncWorker.test.js +2 -12
- package/src/test/1.EcRsaOaepAsyncWorker.workers.test.js +147 -0
- package/src/test/1.FrameworkImport.test.js +170 -0
- package/src/test/1.MedbiqImport.test.js +92 -0
- package/src/test/1.OAuth2FileBasedRemoteIdentityManager.test.js +399 -0
- package/src/test/1.TabStructuredImport.test.js +61 -0
- package/src/test/1.polyfills.test.js +89 -0
- package/src/test/1.schema.competency.pinned.test.js +384 -0
- package/src/test/3.EcFrameworkGraph.test.js +3 -2
- package/src/test/4.schema.test.js +33 -28
- package/src/test/4.skyId.test.js +4 -2
- package/src/test/cypressValue.js +9 -0
- package/src/test/2.EcCompetency.test.js +0 -17
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
require('../../index.js');
|
|
2
|
+
const chai = require('chai');
|
|
3
|
+
const sinon = require('sinon');
|
|
4
|
+
const EcAssertion = require('../org/cass/profile/EcAssertion.js');
|
|
5
|
+
const Assertion = require('../org/cassproject/schema/cass/profile/Assertion.js');
|
|
6
|
+
|
|
7
|
+
const assert = chai.assert;
|
|
8
|
+
|
|
9
|
+
const GETTERS = ['getSubject', 'getAgent', 'getAssertionDate', 'getExpirationDate', 'getEvidences', 'getNegative', 'getDecayFunction'];
|
|
10
|
+
const fakePk = (pem) => ({ toPem: () => pem });
|
|
11
|
+
// copyFrom revives these fields as EcEncryptedValues, so give each a recognisable payload.
|
|
12
|
+
const sealed = (payload) => ({ "@type": "EncryptedValue", "@context": "https://schema.cassproject.org/kbac/0.4", payload });
|
|
13
|
+
|
|
14
|
+
function newAssertion() {
|
|
15
|
+
const a = new EcAssertion();
|
|
16
|
+
a.competency = 'https://x/competency';
|
|
17
|
+
a.level = 'https://x/level';
|
|
18
|
+
a.confidence = 0.75;
|
|
19
|
+
a.subject = sealed('subject');
|
|
20
|
+
a.agent = sealed('agent');
|
|
21
|
+
a.assertionDate = sealed('date');
|
|
22
|
+
a.expirationDate = sealed('expiration');
|
|
23
|
+
a.evidence = [sealed('evidence')];
|
|
24
|
+
a.negative = sealed('negative');
|
|
25
|
+
a.decayFunction = sealed('decay');
|
|
26
|
+
return a;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('EcAssertion.decrypt', () => {
|
|
30
|
+
afterEach(() => sinon.restore());
|
|
31
|
+
|
|
32
|
+
it('returns a plain Assertion with the decrypted fields', async () => {
|
|
33
|
+
const a = newAssertion();
|
|
34
|
+
sinon.stub(a, 'getSubject').resolves(fakePk('SUBJECT'));
|
|
35
|
+
sinon.stub(a, 'getAgent').resolves(fakePk('AGENT'));
|
|
36
|
+
sinon.stub(a, 'getAssertionDate').resolves(123);
|
|
37
|
+
sinon.stub(a, 'getExpirationDate').resolves(456);
|
|
38
|
+
sinon.stub(a, 'getEvidences').resolves(['e1', 'e2']);
|
|
39
|
+
sinon.stub(a, 'getNegative').resolves(true);
|
|
40
|
+
sinon.stub(a, 'getDecayFunction').resolves('t');
|
|
41
|
+
const d = await a.decrypt();
|
|
42
|
+
assert.instanceOf(d, Assertion);
|
|
43
|
+
assert.notInstanceOf(d, EcAssertion);
|
|
44
|
+
assert.equal(d.subject, 'SUBJECT');
|
|
45
|
+
assert.equal(d.agent, 'AGENT');
|
|
46
|
+
assert.equal(d.assertionDate, 123);
|
|
47
|
+
assert.equal(d.expirationDate, 456);
|
|
48
|
+
assert.deepEqual(d.evidence, ['e1', 'e2']);
|
|
49
|
+
assert.strictEqual(d.negative, true);
|
|
50
|
+
assert.equal(d.decayFunction, 't');
|
|
51
|
+
assert.equal(d.competency, 'https://x/competency');
|
|
52
|
+
assert.equal(d.level, 'https://x/level');
|
|
53
|
+
assert.equal(d.confidence, 0.75);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('keeps the copied value when a getter resolves to something falsy', async () => {
|
|
57
|
+
const a = newAssertion();
|
|
58
|
+
sinon.stub(a, 'getSubject').resolves(null);
|
|
59
|
+
sinon.stub(a, 'getAgent').resolves(undefined);
|
|
60
|
+
sinon.stub(a, 'getAssertionDate').resolves(0);
|
|
61
|
+
sinon.stub(a, 'getExpirationDate').resolves(null);
|
|
62
|
+
sinon.stub(a, 'getEvidences').resolves(null);
|
|
63
|
+
sinon.stub(a, 'getNegative').resolves(false);
|
|
64
|
+
sinon.stub(a, 'getDecayFunction').resolves('');
|
|
65
|
+
const d = await a.decrypt();
|
|
66
|
+
assert.equal(d.subject.payload, 'subject');
|
|
67
|
+
assert.equal(d.agent.payload, 'agent');
|
|
68
|
+
assert.equal(d.assertionDate.payload, 'date');
|
|
69
|
+
assert.equal(d.expirationDate.payload, 'expiration');
|
|
70
|
+
assert.equal(d.evidence[0].payload, 'evidence');
|
|
71
|
+
assert.equal(d.negative.payload, 'negative');
|
|
72
|
+
assert.equal(d.decayFunction.payload, 'decay');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('starts every decryption before waiting on any of them', async () => {
|
|
76
|
+
const a = newAssertion();
|
|
77
|
+
let release;
|
|
78
|
+
const gate = new Promise((resolve) => { release = resolve; });
|
|
79
|
+
const stubs = GETTERS.map((g) => sinon.stub(a, g).returns(gate.then(() => null)));
|
|
80
|
+
const eim = {};
|
|
81
|
+
const pending = a.decrypt(eim);
|
|
82
|
+
for (const s of stubs) {
|
|
83
|
+
assert.isTrue(s.calledOnce, s.toString());
|
|
84
|
+
assert.strictEqual(s.firstCall.args[0], eim);
|
|
85
|
+
}
|
|
86
|
+
release();
|
|
87
|
+
await pending;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('rejects when a decryption fails', async () => {
|
|
91
|
+
const a = newAssertion();
|
|
92
|
+
for (const g of GETTERS) sinon.stub(a, g).resolves(null);
|
|
93
|
+
a.getAgent.rejects(new Error('agent failed'));
|
|
94
|
+
let error;
|
|
95
|
+
try { await a.decrypt(); } catch (e) { error = e; }
|
|
96
|
+
assert.equal(error && error.message, 'agent failed');
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
require("../../index.js");
|
|
2
|
+
const { expect } = require("chai");
|
|
3
|
+
const sinon = require("sinon");
|
|
4
|
+
|
|
5
|
+
describe("EcAssertion setters (pinned behaviour)", () => {
|
|
6
|
+
afterEach(() => sinon.restore());
|
|
7
|
+
let pk = (pem) => ({ toPem: () => pem });
|
|
8
|
+
let mk = () => {
|
|
9
|
+
let a = new EcAssertion();
|
|
10
|
+
a.id = "http://x/api/data/a/guid/1";
|
|
11
|
+
return a;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
describe("setSubject / setSubjectAsync", () => {
|
|
15
|
+
for (let async of [false, true]) {
|
|
16
|
+
let label = async ? "setSubjectAsync" : "setSubject";
|
|
17
|
+
let run = async (a, p) => {
|
|
18
|
+
if (!async) return a.setSubject(p);
|
|
19
|
+
let success = sinon.spy();
|
|
20
|
+
await a.setSubjectAsync(p, success, "FAIL");
|
|
21
|
+
expect(success.calledOnceWithExactly()).to.equal(true);
|
|
22
|
+
};
|
|
23
|
+
let stubEncrypt = () => async
|
|
24
|
+
? sinon.stub(EcEncryptedValue, "encryptValueAsync").callsFake((text, id, owners, readers, s, f) => {
|
|
25
|
+
expect(f).to.equal("FAIL");
|
|
26
|
+
s({ enc: text, owners, readers });
|
|
27
|
+
})
|
|
28
|
+
: sinon.stub(EcEncryptedValue, "encryptValue").callsFake(async (text, id, owners, readers) => ({ enc: text, owners, readers }));
|
|
29
|
+
it(`${label}: encrypts the pem for owners and a copy of the readers`, async () => {
|
|
30
|
+
let enc = stubEncrypt();
|
|
31
|
+
let a = mk();
|
|
32
|
+
a.owner = ["o1"];
|
|
33
|
+
a.reader = ["r1"];
|
|
34
|
+
await run(a, pk("PEM"));
|
|
35
|
+
expect(a.subject).to.deep.equal({ enc: "PEM", owners: ["o1"], readers: ["r1", "PEM"] });
|
|
36
|
+
expect(a.reader).to.deep.equal(["r1"]);
|
|
37
|
+
expect(enc.firstCall.args[1]).to.equal(a.id);
|
|
38
|
+
expect(enc.firstCall.args[3]).to.not.equal(a.reader);
|
|
39
|
+
});
|
|
40
|
+
it(`${label}: merges the previous subject's owners and readers`, async () => {
|
|
41
|
+
stubEncrypt();
|
|
42
|
+
let a = mk();
|
|
43
|
+
a.owner = ["o1"];
|
|
44
|
+
a.reader = [{ k: "r1" }];
|
|
45
|
+
a.subject = { owner: ["so"], reader: ["sr"] };
|
|
46
|
+
await run(a, pk("PEM"));
|
|
47
|
+
expect(a.subject).to.deep.equal({ enc: "PEM", owners: ["so", "o1"], readers: [{ k: "r1" }, "sr", "PEM"] });
|
|
48
|
+
expect(a.subject.readers[0]).to.not.equal(a.reader[0]);
|
|
49
|
+
});
|
|
50
|
+
it(`${label}: copies a Proxy-wrapped reader array (as reactive UI state would hold)`, async () => {
|
|
51
|
+
stubEncrypt();
|
|
52
|
+
let a = mk();
|
|
53
|
+
a.reader = new Proxy(["r1"], {});
|
|
54
|
+
await run(a, pk("PEM"));
|
|
55
|
+
expect(a.subject.readers).to.deep.equal(["r1", "PEM"]);
|
|
56
|
+
expect(Array.isArray(a.subject.readers)).to.equal(true);
|
|
57
|
+
});
|
|
58
|
+
it(`${label}: works without owners or readers`, async () => {
|
|
59
|
+
stubEncrypt();
|
|
60
|
+
let a = mk();
|
|
61
|
+
await run(a, pk("PEM"));
|
|
62
|
+
expect(a.subject).to.deep.equal({ enc: "PEM", owners: [], readers: ["PEM"] });
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("encrypted field setters", () => {
|
|
68
|
+
let fields = [
|
|
69
|
+
["setAgent", "agent", pk("APEM"), "APEM"],
|
|
70
|
+
["setAssertionDate", "assertionDate", 1234, "1234"],
|
|
71
|
+
["setExpirationDate", "expirationDate", 5678, "5678"],
|
|
72
|
+
["setDecayFunction", "decayFunction", "t^2", "t^2"],
|
|
73
|
+
["setNegative", "negative", true, "true"],
|
|
74
|
+
];
|
|
75
|
+
for (let [method, field, value, text] of fields) {
|
|
76
|
+
it(`${method} encrypts with the subject's owners and readers`, async () => {
|
|
77
|
+
let enc = sinon.stub(EcEncryptedValue, "encryptValue").callsFake(async (t, id, o, r) => ({ t, id, o, r }));
|
|
78
|
+
let a = mk();
|
|
79
|
+
a.subject = { owner: ["so"], reader: ["sr"] };
|
|
80
|
+
let ret = await a[method](value);
|
|
81
|
+
expect(ret).to.equal(undefined);
|
|
82
|
+
expect(a[field]).to.deep.equal({ t: text, id: a.id, o: ["so"], r: ["sr"] });
|
|
83
|
+
expect(enc.calledOnce).to.equal(true);
|
|
84
|
+
});
|
|
85
|
+
it(`${method}Async sets the field from the callback and then calls success`, () => {
|
|
86
|
+
let order = [];
|
|
87
|
+
sinon.stub(EcEncryptedValue, "encryptValueAsync").callsFake((t, id, o, r, s, f) => {
|
|
88
|
+
expect(f).to.equal("FAIL");
|
|
89
|
+
s({ t, id, o, r });
|
|
90
|
+
return "RET";
|
|
91
|
+
});
|
|
92
|
+
let a = mk();
|
|
93
|
+
a.subject = { owner: ["so"], reader: ["sr"] };
|
|
94
|
+
let ret = a[method + "Async"](value, () => order.push(JSON.stringify(a[field])), "FAIL");
|
|
95
|
+
expect(ret).to.equal("RET");
|
|
96
|
+
expect(order).to.deep.equal([JSON.stringify({ t: text, id: a.id, o: ["so"], r: ["sr"] })]);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
it("setEvidence encrypts each evidence in order", async () => {
|
|
100
|
+
let calls = [];
|
|
101
|
+
sinon.stub(EcEncryptedValue, "encryptValue").callsFake(async (t) => {
|
|
102
|
+
calls.push(t);
|
|
103
|
+
return "E:" + t;
|
|
104
|
+
});
|
|
105
|
+
let a = mk();
|
|
106
|
+
a.subject = { owner: ["so"], reader: ["sr"] };
|
|
107
|
+
await a.setEvidence(["e1", "e2", "e3"]);
|
|
108
|
+
expect(calls).to.deep.equal(["e1", "e2", "e3"]);
|
|
109
|
+
expect(a.evidence).to.deep.equal(["E:e1", "E:e2", "E:e3"]);
|
|
110
|
+
await a.setEvidence([]);
|
|
111
|
+
expect(a.evidence).to.deep.equal([]);
|
|
112
|
+
});
|
|
113
|
+
it("setEvidence leaves evidence unchanged if an encryption fails", async () => {
|
|
114
|
+
sinon.stub(EcEncryptedValue, "encryptValue")
|
|
115
|
+
.onFirstCall().resolves("ok")
|
|
116
|
+
.onSecondCall().rejects(new Error("bad"));
|
|
117
|
+
let a = mk();
|
|
118
|
+
a.subject = { owner: [], reader: [] };
|
|
119
|
+
a.evidence = ["old"];
|
|
120
|
+
let err = null;
|
|
121
|
+
try {
|
|
122
|
+
await a.setEvidence(["e1", "e2"]);
|
|
123
|
+
} catch (e) {
|
|
124
|
+
err = e;
|
|
125
|
+
}
|
|
126
|
+
expect(err.message).to.equal("bad");
|
|
127
|
+
expect(a.evidence).to.deep.equal(["old"]);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("addReader / removeReader", () => {
|
|
132
|
+
for (let method of ["addReader", "removeReader"]) {
|
|
133
|
+
it(`${method} updates every encrypted field, each evidence in order, then the object`, async () => {
|
|
134
|
+
let calls = [];
|
|
135
|
+
let ev = (name) => ({ [method]: async (r, eim) => calls.push([name, r, eim]) });
|
|
136
|
+
let a = mk();
|
|
137
|
+
a.agent = ev("agent");
|
|
138
|
+
a.assertionDate = ev("assertionDate");
|
|
139
|
+
a.decayFunction = ev("decayFunction");
|
|
140
|
+
a.evidence = [ev("e1"), ev("e2")];
|
|
141
|
+
a.expirationDate = ev("expirationDate");
|
|
142
|
+
a.negative = ev("negative");
|
|
143
|
+
a.subject = ev("subject");
|
|
144
|
+
a.reader = ["x"];
|
|
145
|
+
let reader = { toPem: () => "R" };
|
|
146
|
+
await a[method](reader, "EIM");
|
|
147
|
+
expect(calls.map((c) => c[0])).to.deep.equal(
|
|
148
|
+
["agent", "assertionDate", "decayFunction", "e1", "e2", "expirationDate", "negative", "subject"]);
|
|
149
|
+
expect(calls.every((c) => c[1] === reader && c[2] === "EIM")).to.equal(true);
|
|
150
|
+
expect(a.reader).to.deep.equal(method == "addReader" ? ["x", "R"] : ["x"]);
|
|
151
|
+
});
|
|
152
|
+
it(`${method} with no encrypted fields only updates the object`, async () => {
|
|
153
|
+
let a = mk();
|
|
154
|
+
a.evidence = [];
|
|
155
|
+
await a[method]({ toPem: () => "R" });
|
|
156
|
+
expect(a.reader).to.deep.equal(method == "addReader" ? ["R"] : []);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
require('../../index.js');
|
|
2
|
+
const chai = require('chai');
|
|
3
|
+
const sinon = require('sinon');
|
|
4
|
+
const forge = require('node-forge');
|
|
5
|
+
const EcPpk = require('../com/eduworks/ec/crypto/EcPpk.js');
|
|
6
|
+
const EcPk = require('../com/eduworks/ec/crypto/EcPk.js');
|
|
7
|
+
const EcRsaOaepAsync = require('../com/eduworks/ec/crypto/EcRsaOaepAsync.js');
|
|
8
|
+
const EcRsaOaepAsyncWorker = require('../com/eduworks/ec/crypto/EcRsaOaepAsyncWorker.js');
|
|
9
|
+
const EcAesCtrAsync = require('../com/eduworks/ec/crypto/EcAesCtrAsync.js');
|
|
10
|
+
const EcCrypto = require('../com/eduworks/ec/crypto/EcCrypto.js');
|
|
11
|
+
|
|
12
|
+
const assert = chai.assert;
|
|
13
|
+
const isNode = typeof process !== 'undefined' && process.versions?.node != null && typeof window === 'undefined';
|
|
14
|
+
|
|
15
|
+
const PEM = "-----BEGIN RSA PRIVATE KEY-----MIIEpAIBAAKCAQEAz4BiFucFE9bNcKfGD+e6aPRHl402YM4Z6nrurDRNlnwsWpsCoZasPLkjC314pVtHAI2duZo+esGKDloBsiLxASRJo3R2XiXVh2Y8U1RcHA5mWL4tMG5UY2d0libpNEHbHPNBmooVYpA2yhxN/vGibIk8x69uZWxJcFOxOg6zWG8EjF8UMgGnRCVSMTY3THhTlfZ0cGUzvrfb7OvHUgdCe285XkmYkj/V9P/m7hbWoOyJAJSTOm4/s6fIKpl72lblfN7bKaxTCsJp6/rQdmUeo+PIaa2lDOfo7dWbuTMcqkZ93kispNfYYhsEGUGlCsrrVWhlve8MenO4GdLsFP+HRwIDAQABAoIBAGaQpOuBIYde44lNxJ7UAdYi+Mg2aqyK81Btl0/TQo6hriLTAAfzPAt/z4y8ZkgFyCDD3zSAw2VWCPFzF+d/UfUohKWgyWlb9iHJLQRbbHQJwhkXV6raviesWXpmnVrROocizkie/FcNxac9OmhL8+cGJt7lHgJP9jTpiW6TGZ8ZzM8KBH2l80x9AWdvCjsICuPIZRjc706HtkKZzTROtq6Z/F4Gm0uWRnwAZrHTRpnh8qjtdBLYFrdDcUoFtzOM6UVRmocTfsNe4ntPpvwY2aGTWY7EmTj1kteMJ+fCQFIS+KjyMWQHsN8yQNfD5/j2uv6/BdSkO8uorGSJT6DwmTECgYEA8ydoQ4i58+A1udqA+fujM0Zn46++NTehFe75nqIt8rfQgoduBam3lE5IWj2U2tLQeWxQyr1ZJkLbITtrAI3PgfMnuFAii+cncwFo805Fss/nbKx8K49vBuCEAq3MRhLjWy3ZvIgUHj67jWvl50dbNqc7TUguxhS4BxGr/cPPkP0CgYEA2nbJPGzSKhHTETL37NWIUAdU9q/6NVRISRRXeRqZYwE1VPzs2sIUxA8zEDBHX7OtvCKzvZy1Lg5Unx1nh4nCEVkbW/8npLlRG2jOcZJF6NRfhzwLz3WMIrP6j9SmjJaB+1mnrTjfsg36tDEPDjjJLjJHCx9z/qRJh1v4bh4aPpMCgYACG31T2IOEEZVlnvcvM3ceoqWT25oSbAEBZ6jSLyWmzOEJwJK7idUFfAg0gAQiQWF9K+snVqzHIB02FIXA43nA7pKRjmA+RiqZXJHEShFgk1y2HGiXGA8mSBvcyhTTJqbBy4vvjl5eRLzrZNwBPSUVPC3PZajCHrvZk9WhxWivIQKBgQCzCu1MH2dy4R7ZlqsIJ8zKweeJMZpfQI7pjclO0FTrhh7+Yzd+5db9A/P2jYrBTVHSwaILgTYf49DIguHJfEZXz26TzB7iapqlWxTukVHISt1ryPNo+E58VoLAhChnSiaHJ+g7GESE+d4A9cAACNwgh0YgQIvhIyW70M1e+j7KDwKBgQDQSBLFDFmvvTP3sIRAr1+0OZWd1eRcwdhs0U9GwootoCoUP/1Y64pqukT6B9oIB/No9Nyn8kUX3/ZDtCslaGKEUGMJXQ4hc5J+lq0tSi9ZWBdhqOuMPEfUF3IxW+9yeILP4ppUBn1m5MVOWg5CvuuEeCmy4bhMaUErUlHZ78t5cA==-----END RSA PRIVATE KEY-----";
|
|
16
|
+
const PK_PEM = "-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz4BiFucFE9bNcKfGD+e6aPRHl402YM4Z6nrurDRNlnwsWpsCoZasPLkjC314pVtHAI2duZo+esGKDloBsiLxASRJo3R2XiXVh2Y8U1RcHA5mWL4tMG5UY2d0libpNEHbHPNBmooVYpA2yhxN/vGibIk8x69uZWxJcFOxOg6zWG8EjF8UMgGnRCVSMTY3THhTlfZ0cGUzvrfb7OvHUgdCe285XkmYkj/V9P/m7hbWoOyJAJSTOm4/s6fIKpl72lblfN7bKaxTCsJp6/rQdmUeo+PIaa2lDOfo7dWbuTMcqkZ93kispNfYYhsEGUGlCsrrVWhlve8MenO4GdLsFP+HRwIDAQAB-----END PUBLIC KEY-----";
|
|
17
|
+
const PK_PKCS1_PEM = "-----BEGIN RSA PUBLIC KEY-----MIIBCgKCAQEAz4BiFucFE9bNcKfGD+e6aPRHl402YM4Z6nrurDRNlnwsWpsCoZasPLkjC314pVtHAI2duZo+esGKDloBsiLxASRJo3R2XiXVh2Y8U1RcHA5mWL4tMG5UY2d0libpNEHbHPNBmooVYpA2yhxN/vGibIk8x69uZWxJcFOxOg6zWG8EjF8UMgGnRCVSMTY3THhTlfZ0cGUzvrfb7OvHUgdCe285XkmYkj/V9P/m7hbWoOyJAJSTOm4/s6fIKpl72lblfN7bKaxTCsJp6/rQdmUeo+PIaa2lDOfo7dWbuTMcqkZ93kispNfYYhsEGUGlCsrrVWhlve8MenO4GdLsFP+HRwIDAQAB-----END RSA PUBLIC KEY-----";
|
|
18
|
+
const TEXT = "hello ᚠ world";
|
|
19
|
+
const SIG_SHA1 = "d5IBsi6/wwUEfF+3IuYMoac7yOaSm/r8+1YfRqKKk9rxgep9cjO1Y0Pyk1rbOzQIpQ4fO8SIqigdag9pbx5kxClPm452SfQplpzj05kPnFv58YB9ZWA/wb7nSFV7cLqHGfiRqAr/+hvedbVToLBUsc8UVLauq36WZW0M7cbqR74ZUxRPNftc7b/gGlIsiHNb0KA2H+HF4XcoDVRKaGIrJk0w3ThW9swSD9gU04131WRFw/KVGhfUnmUAuuVYt6JiKx2Rj5MZvzKtedksP6ytJPqiDKahBRZBiwCnNplEQXJKxSRucMUzmwyVPSHxQhihl0MQZZU4otVcE8EJFASEEw==";
|
|
20
|
+
const SIG_SHA256 = "xZZkzhAqea1SuQFWR9KoDGo2sMY17wb40azbWbnF1iEnf3RNb3wQgKnsMWfNCuaEuc6wLC8dvhFFFrgOPB70/xDgnNjBsXREFCQ+sZyeJvMA4sg4m5reu9bm6XpPaVzL/S7liHlseb6U15Tr+TbucajZieMdwNNhQjUR0R5+WxJBkhUoAtMao2HNnMbAQ3A7ILJHWp1HkaJpabp7zTlHERMB/6ymqMQFGpzSZnZpdl5NxDi/qWf7v6G+TsTGv26NTb0/jfGkCUcy7jvjR79qXP1G0zVB4K7cnxrEHiilkfmPMPC7bxdW6s1eA/gwrV4+4qPqtLJW5de60gpOCJJ3gQ==";
|
|
21
|
+
const AES_SECRET = "AAECAwQFBgcICQoLDA0ODw==";
|
|
22
|
+
const AES_IV = "EBESExQVFhcYGRobHB0eHw==";
|
|
23
|
+
|
|
24
|
+
const sha256 = (s) => forge.md.sha256.create().update(typeof s === 'string' ? s : JSON.stringify(s), 'utf8').digest().toHex();
|
|
25
|
+
|
|
26
|
+
describe('EcPk and EcPpk encodings', () => {
|
|
27
|
+
it('EcPpk PEM and JWK encodings are unchanged', () => {
|
|
28
|
+
const ppk = EcPpk.fromPem(PEM);
|
|
29
|
+
delete ppk.defaultPem;
|
|
30
|
+
delete ppk.jwk;
|
|
31
|
+
assert.equal(sha256(ppk.toPem()), 'f08b141ee4e918684781eca1aa13cf16a3631674d56d8f65b70e9ba8bbbd8b3a');
|
|
32
|
+
assert.equal(ppk.toPem(), PEM);
|
|
33
|
+
assert.equal(sha256(ppk.toPkcs1Pem()), 'f08b141ee4e918684781eca1aa13cf16a3631674d56d8f65b70e9ba8bbbd8b3a');
|
|
34
|
+
assert.equal(sha256(ppk.toPkcs8Pem()), 'aedcee9d0922723128ede50a0390df163b04fc71e87884d7609ac10d5f2f1b3d');
|
|
35
|
+
assert.notMatch(ppk.toPkcs8Pem(), /[\r\n]/);
|
|
36
|
+
assert.match(ppk.toPkcs8Pem(), /^-----BEGIN PRIVATE KEY-----[A-Za-z0-9+/=]+-----END PRIVATE KEY-----$/);
|
|
37
|
+
assert.equal(sha256(ppk.toJwk()), '4b912898e05a4c2001d5cca115590491efd85b8a19de8f18be486d46642fec69');
|
|
38
|
+
for (const v of Object.values(ppk.toJwk()))
|
|
39
|
+
assert.notMatch(v, /[+/=]/);
|
|
40
|
+
});
|
|
41
|
+
it('EcPk PEM and JWK encodings are unchanged', () => {
|
|
42
|
+
const pk = EcPpk.fromPem(PEM).toPk();
|
|
43
|
+
delete pk.defaultPem;
|
|
44
|
+
delete pk.jwk;
|
|
45
|
+
assert.equal(pk.toPem(), PK_PEM);
|
|
46
|
+
assert.equal(sha256(pk.toPem()), '9b54f7aa5d9949eb22252c69f0894dfe280d2cf999f60b30a4d9fa90f45c23d4');
|
|
47
|
+
assert.equal(pk.toPkcs1Pem(), PK_PKCS1_PEM);
|
|
48
|
+
assert.equal(sha256(pk.toPkcs1Pem()), '0076d5976edd7de19feb6c0e1d49a8845a66afb35916aeeb5d765601e145f15f');
|
|
49
|
+
assert.equal(pk.toPkcs8Pem(), PK_PEM);
|
|
50
|
+
assert.equal(sha256(pk.toJwk()), 'd8899e312b58f4b3284a4bde1a8c31af0d025fb04e6401dbf55cd34f4ff71b51');
|
|
51
|
+
assert.deepEqual(Object.keys(pk.toJwk()), ['kty', 'n', 'e']);
|
|
52
|
+
assert.equal(pk.toJwk().e, 'AQAB');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('EcRsaOaepAsync signatures', () => {
|
|
57
|
+
it('sign (sha1) gives the same signature with and without a cached key', async () => {
|
|
58
|
+
const ppk = EcPpk.fromPem(PEM);
|
|
59
|
+
delete ppk.signKey;
|
|
60
|
+
assert.equal(await EcRsaOaepAsync.sign(ppk, TEXT), SIG_SHA1);
|
|
61
|
+
assert.equal(await EcRsaOaepAsync.sign(ppk, TEXT), SIG_SHA1);
|
|
62
|
+
});
|
|
63
|
+
it('sign (sha1) passes the signature to the success callback', async () => {
|
|
64
|
+
const ppk = EcPpk.fromPem(PEM);
|
|
65
|
+
delete ppk.signKey;
|
|
66
|
+
let seen = null;
|
|
67
|
+
const r = await EcRsaOaepAsync.sign(ppk, TEXT, (s) => { seen = s; return 'cb'; }, null);
|
|
68
|
+
assert.equal(seen, SIG_SHA1);
|
|
69
|
+
assert.equal(r, 'cb');
|
|
70
|
+
});
|
|
71
|
+
it('signSha256 gives the same signature with and without a cached key', async () => {
|
|
72
|
+
const ppk = EcPpk.fromPem(PEM);
|
|
73
|
+
delete ppk.signKey256;
|
|
74
|
+
assert.equal(await EcRsaOaepAsync.signSha256(ppk, TEXT), SIG_SHA256);
|
|
75
|
+
assert.equal(await EcRsaOaepAsync.signSha256(ppk, TEXT), SIG_SHA256);
|
|
76
|
+
});
|
|
77
|
+
it('verify (sha1) with and without a cached key', async () => {
|
|
78
|
+
const pk = EcPk.fromPem(PK_PEM);
|
|
79
|
+
delete pk.signKey;
|
|
80
|
+
assert.strictEqual(await EcRsaOaepAsync.verify(pk, TEXT, SIG_SHA1), true);
|
|
81
|
+
assert.isDefined(pk.signKey);
|
|
82
|
+
assert.strictEqual(await EcRsaOaepAsync.verify(pk, TEXT, SIG_SHA1), true);
|
|
83
|
+
assert.strictEqual(await EcRsaOaepAsync.verify(pk, TEXT + "x", SIG_SHA1), false);
|
|
84
|
+
delete pk.signKey;
|
|
85
|
+
assert.strictEqual(await EcRsaOaepAsync.verify(pk, TEXT + "x", SIG_SHA1), false);
|
|
86
|
+
});
|
|
87
|
+
it('verifySha256 with and without a cached key', async () => {
|
|
88
|
+
const pk = EcPk.fromPem(PK_PEM);
|
|
89
|
+
delete pk.signKey256;
|
|
90
|
+
assert.strictEqual(await EcRsaOaepAsync.verifySha256(pk, TEXT, SIG_SHA256), true);
|
|
91
|
+
assert.isDefined(pk.signKey256);
|
|
92
|
+
assert.strictEqual(await EcRsaOaepAsync.verifySha256(pk, TEXT, SIG_SHA256), true);
|
|
93
|
+
assert.strictEqual(await EcRsaOaepAsync.verifySha256(pk, TEXT, SIG_SHA1), false);
|
|
94
|
+
delete pk.signKey256;
|
|
95
|
+
assert.strictEqual(await EcRsaOaepAsync.verifySha256(pk, TEXT, SIG_SHA1), false);
|
|
96
|
+
});
|
|
97
|
+
it('verify rejects when the key cannot be imported', async () => {
|
|
98
|
+
const pk = EcPk.fromPem(PK_PEM);
|
|
99
|
+
const saved = pk.signKey;
|
|
100
|
+
delete pk.signKey;
|
|
101
|
+
const jwk = sinon.stub(pk, 'toJwk').returns({ kty: 'RSA' });
|
|
102
|
+
try {
|
|
103
|
+
let failed = false;
|
|
104
|
+
await EcRsaOaepAsync.verify(pk, TEXT, SIG_SHA1).catch(() => { failed = true; });
|
|
105
|
+
assert.isTrue(failed);
|
|
106
|
+
} finally {
|
|
107
|
+
jwk.restore();
|
|
108
|
+
pk.signKey = saved;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('EcAesCtrAsync', () => {
|
|
114
|
+
let caching;
|
|
115
|
+
beforeEach(() => { caching = EcCrypto.caching; });
|
|
116
|
+
afterEach(() => { EcCrypto.caching = caching; });
|
|
117
|
+
|
|
118
|
+
it('encrypts to the pinned ciphertext', async () => {
|
|
119
|
+
assert.equal(await EcAesCtrAsync.encrypt("plain ascii text", AES_SECRET, AES_IV), "d5KOHY/1Yh3zZ4cx+vHq5w==");
|
|
120
|
+
assert.equal(await EcAesCtrAsync.encrypt(TEXT, AES_SECRET, AES_IV), "b5uDGI714vQwLpl+/Pj2");
|
|
121
|
+
});
|
|
122
|
+
it('encrypt passes the ciphertext to the success callback', async () => {
|
|
123
|
+
let seen = null;
|
|
124
|
+
await EcAesCtrAsync.encrypt(TEXT, AES_SECRET, AES_IV, (c) => { seen = c; }, null);
|
|
125
|
+
assert.equal(seen, "b5uDGI714vQwLpl+/Pj2");
|
|
126
|
+
});
|
|
127
|
+
it('decrypts without caching', async () => {
|
|
128
|
+
EcCrypto.caching = false;
|
|
129
|
+
delete EcCrypto.decryptionCache[AES_SECRET + AES_IV + "b5uDGI714vQwLpl+/Pj2"];
|
|
130
|
+
assert.equal(await EcAesCtrAsync.decrypt("b5uDGI714vQwLpl+/Pj2", AES_SECRET, AES_IV), TEXT);
|
|
131
|
+
assert.isUndefined(EcCrypto.decryptionCache[AES_SECRET + AES_IV + "b5uDGI714vQwLpl+/Pj2"]);
|
|
132
|
+
});
|
|
133
|
+
it('decrypts with caching and fills the cache', async () => {
|
|
134
|
+
EcCrypto.caching = true;
|
|
135
|
+
const key = AES_SECRET + AES_IV + "b5uDGI714vQwLpl+/Pj2";
|
|
136
|
+
delete EcCrypto.decryptionCache[key];
|
|
137
|
+
assert.equal(await EcAesCtrAsync.decrypt("b5uDGI714vQwLpl+/Pj2", AES_SECRET, AES_IV), TEXT);
|
|
138
|
+
assert.equal(EcCrypto.decryptionCache[key], TEXT);
|
|
139
|
+
delete EcCrypto.decryptionCache[key];
|
|
140
|
+
});
|
|
141
|
+
it('decrypt rejects on a bad key', async () => {
|
|
142
|
+
EcCrypto.caching = false;
|
|
143
|
+
let failed = null;
|
|
144
|
+
await EcAesCtrAsync.decrypt("b5uDGI714vQwLpl+/Pj2", "AAEC", AES_IV, null, (e) => { failed = e; });
|
|
145
|
+
assert.isNotNull(failed);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
(isNode ? describe : describe.skip)('EcAesCtrAsync FIPS toggling errors', () => {
|
|
150
|
+
const realCrypto = isNode ? require('crypto') : null;
|
|
151
|
+
const modulePath = isNode ? require.resolve('../com/eduworks/ec/crypto/EcAesCtrAsync.js') : null;
|
|
152
|
+
let fips;
|
|
153
|
+
function freshModule() {
|
|
154
|
+
// The module keeps fipsForced / inWorker flags; load a private copy so they don't leak.
|
|
155
|
+
const saved = require.cache[modulePath];
|
|
156
|
+
delete require.cache[modulePath];
|
|
157
|
+
try {
|
|
158
|
+
return require(modulePath);
|
|
159
|
+
} finally {
|
|
160
|
+
require.cache[modulePath] = saved;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
beforeEach(() => {
|
|
164
|
+
fips = process.env.FIPS;
|
|
165
|
+
process.env.FIPS = "1";
|
|
166
|
+
});
|
|
167
|
+
afterEach(() => {
|
|
168
|
+
if (fips === undefined) delete process.env.FIPS;
|
|
169
|
+
else process.env.FIPS = fips;
|
|
170
|
+
sinon.restore();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
for (const [method, current] of [['fipsOn', 0], ['fipsOff', 1]]) {
|
|
174
|
+
it(method + ': ERR_CRYPTO_FIPS_FORCED stops further toggling', () => {
|
|
175
|
+
const M = freshModule();
|
|
176
|
+
sinon.stub(realCrypto, 'getFips').returns(current);
|
|
177
|
+
const set = sinon.stub(realCrypto, 'setFips').throws(new Error("boom ERR_CRYPTO_FIPS_FORCED"));
|
|
178
|
+
const report = sinon.stub(global.auditLogger, 'report');
|
|
179
|
+
M[method]();
|
|
180
|
+
assert.equal(set.callCount, 1);
|
|
181
|
+
assert.deepEqual(set.firstCall.args, [current == 0]);
|
|
182
|
+
M.fipsOn();
|
|
183
|
+
M.fipsOff();
|
|
184
|
+
assert.equal(set.callCount, 1);
|
|
185
|
+
assert.equal(realCrypto.getFips.callCount, 1);
|
|
186
|
+
assert.equal(report.callCount, 0);
|
|
187
|
+
});
|
|
188
|
+
it(method + ': ERR_WORKER_UNSUPPORTED_OPERATION stops further toggling', () => {
|
|
189
|
+
const M = freshModule();
|
|
190
|
+
sinon.stub(realCrypto, 'getFips').returns(current);
|
|
191
|
+
const set = sinon.stub(realCrypto, 'setFips').throws(new Error("ERR_WORKER_UNSUPPORTED_OPERATION here"));
|
|
192
|
+
const report = sinon.stub(global.auditLogger, 'report');
|
|
193
|
+
M[method]();
|
|
194
|
+
M[method]();
|
|
195
|
+
M.fipsOn();
|
|
196
|
+
assert.equal(set.callCount, 1);
|
|
197
|
+
assert.equal(report.callCount, 0);
|
|
198
|
+
});
|
|
199
|
+
it(method + ': other errors are reported and toggling continues', () => {
|
|
200
|
+
const M = freshModule();
|
|
201
|
+
sinon.stub(realCrypto, 'getFips').returns(current);
|
|
202
|
+
const err = new Error("something else");
|
|
203
|
+
const set = sinon.stub(realCrypto, 'setFips').throws(err);
|
|
204
|
+
const report = sinon.stub(global.auditLogger, 'report');
|
|
205
|
+
M[method]();
|
|
206
|
+
M[method]();
|
|
207
|
+
assert.equal(set.callCount, 2);
|
|
208
|
+
assert.equal(report.callCount, 2);
|
|
209
|
+
assert.deepEqual(report.firstCall.args.slice(2), ["EcAesCtrAsyncFips", "ERR_CRYPTO_FIPS", err]);
|
|
210
|
+
});
|
|
211
|
+
it(method + ': does nothing when FIPS is already in the wanted state', () => {
|
|
212
|
+
const M = freshModule();
|
|
213
|
+
sinon.stub(realCrypto, 'getFips').returns(1 - current);
|
|
214
|
+
const set = sinon.stub(realCrypto, 'setFips');
|
|
215
|
+
M[method]();
|
|
216
|
+
assert.equal(set.callCount, 0);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
it('does nothing when FIPS is not requested', () => {
|
|
220
|
+
delete process.env.FIPS;
|
|
221
|
+
const M = freshModule();
|
|
222
|
+
const get = sinon.stub(realCrypto, 'getFips').returns(0);
|
|
223
|
+
const set = sinon.stub(realCrypto, 'setFips');
|
|
224
|
+
M.fipsOn();
|
|
225
|
+
M.fipsOff();
|
|
226
|
+
assert.equal(get.callCount, 0);
|
|
227
|
+
assert.equal(set.callCount, 0);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
(isNode ? describe : describe.skip)('EcRsaOaepAsyncWorker.createWorker', () => {
|
|
232
|
+
const path = isNode ? require('path') : null;
|
|
233
|
+
const url = isNode ? require('url') : null;
|
|
234
|
+
let savedWorker, savedW;
|
|
235
|
+
beforeEach(() => {
|
|
236
|
+
savedWorker = global.Worker;
|
|
237
|
+
savedW = EcRsaOaepAsyncWorker.w;
|
|
238
|
+
});
|
|
239
|
+
afterEach(() => {
|
|
240
|
+
global.Worker = savedWorker;
|
|
241
|
+
EcRsaOaepAsyncWorker.w = savedW;
|
|
242
|
+
sinon.restore();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('falls back through the worker scripts and replaces a worker that errors', () => {
|
|
246
|
+
const created = [];
|
|
247
|
+
global.Worker = class FakeWorker {
|
|
248
|
+
constructor(arg) {
|
|
249
|
+
created.push(arg);
|
|
250
|
+
if (created.length <= 2) throw new Error("no worker " + created.length);
|
|
251
|
+
this.arg = arg;
|
|
252
|
+
}
|
|
253
|
+
addEventListener() { }
|
|
254
|
+
};
|
|
255
|
+
const report = sinon.stub(global.auditLogger, 'report');
|
|
256
|
+
EcRsaOaepAsyncWorker.w = [];
|
|
257
|
+
EcRsaOaepAsyncWorker.createWorker(3);
|
|
258
|
+
const dir = path.dirname(require.resolve('../com/eduworks/ec/crypto/EcRsaOaepAsyncWorker.js'));
|
|
259
|
+
assert.equal(created.length, 3);
|
|
260
|
+
assert.equal(created[0].href, url.pathToFileURL(path.resolve(dir, 'forgeAsyncNode.js')).href);
|
|
261
|
+
assert.equal(created[1].href, url.pathToFileURL(path.resolve(dir, 'forgeAsync.js')).href);
|
|
262
|
+
assert.equal(created[2], path.resolve(dir, 'forgeAsync.js'));
|
|
263
|
+
assert.equal(report.callCount, 2);
|
|
264
|
+
const first = EcRsaOaepAsyncWorker.w[3];
|
|
265
|
+
assert.equal(first._worker.arg, created[2]);
|
|
266
|
+
|
|
267
|
+
first._worker.onerror({ type: 'error' });
|
|
268
|
+
assert.equal(created.length, 4);
|
|
269
|
+
assert.equal(created[3], path.resolve(dir, 'cass-editor/forgeAsync.js'));
|
|
270
|
+
assert.notStrictEqual(EcRsaOaepAsyncWorker.w[3], first);
|
|
271
|
+
assert.equal(EcRsaOaepAsyncWorker.w[3]._worker.arg, created[3]);
|
|
272
|
+
assert.equal(report.callCount, 3);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
(isNode ? describe : describe.skip)('forgeAsyncNode worker messages', () => {
|
|
277
|
+
after(() => EcRsaOaepAsyncWorker.teardown());
|
|
278
|
+
it('answers unknown commands and reports command errors', async () => {
|
|
279
|
+
EcRsaOaepAsyncWorker.initWorker();
|
|
280
|
+
const w = EcRsaOaepAsyncWorker.w[0];
|
|
281
|
+
assert.equal(await w.postMessage({ cmd: 'nope', origin: 'cassproject' }), 'Unknown command: undefined');
|
|
282
|
+
const r = await w.postMessage({ cmd: 'signRsaOaep', ppk: 'not a pem', text: 'x', origin: 'cassproject' });
|
|
283
|
+
assert.property(r, 'error');
|
|
284
|
+
assert.equal(await w.postMessage({ cmd: 'signRsaOaep', ppk: PEM, text: TEXT, origin: 'cassproject' }), SIG_SHA1);
|
|
285
|
+
});
|
|
286
|
+
it('passes a rejected promise back as an error', async () => {
|
|
287
|
+
EcRsaOaepAsyncWorker.initWorker();
|
|
288
|
+
const w = EcRsaOaepAsyncWorker.w[1];
|
|
289
|
+
let failed = null;
|
|
290
|
+
await w.postMessage({ cmd: 'decryptAesCtr', text: 'b5uDGI714vQwLpl+/Pj2', secret: 'AAEC', iv: AES_IV, origin: 'cassproject' }).catch((e) => { failed = e; });
|
|
291
|
+
assert.instanceOf(failed, Error);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require('../../index.js');
|
|
2
|
+
const chai = require('chai');
|
|
3
|
+
const sinon = require('sinon');
|
|
4
|
+
const realCrypto = require('crypto');
|
|
5
|
+
const EcCrypto = require('../com/eduworks/ec/crypto/EcCrypto.js');
|
|
6
|
+
|
|
7
|
+
const assert = chai.assert;
|
|
8
|
+
|
|
9
|
+
const UUID = /^[0-9a-f]{8}-c455-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
10
|
+
|
|
11
|
+
// Fixes the clock and the random source (whichever one the implementation uses) to
|
|
12
|
+
// the nibble k, so the output is deterministic.
|
|
13
|
+
function withFixedEntropy(k, fn) {
|
|
14
|
+
const clock = sinon.useFakeTimers({ now: 1700000000000 });
|
|
15
|
+
const stubs = [sinon.stub(Math, 'random').returns(k / 16)];
|
|
16
|
+
const fill = (array) => { array.fill(k); return array; };
|
|
17
|
+
stubs.push(sinon.stub(globalThis.crypto, 'getRandomValues').callsFake(fill));
|
|
18
|
+
// Bundled for the browser, the crypto module has no webcrypto.
|
|
19
|
+
if (realCrypto.webcrypto && realCrypto.webcrypto !== globalThis.crypto)
|
|
20
|
+
stubs.push(sinon.stub(realCrypto.webcrypto, 'getRandomValues').callsFake(fill));
|
|
21
|
+
try {
|
|
22
|
+
return fn();
|
|
23
|
+
} finally {
|
|
24
|
+
stubs.forEach((s) => s.restore());
|
|
25
|
+
clock.restore();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
for (const [name, generate] of [['EcCrypto.generateUUID', () => EcCrypto.generateUUID()], ['global.generateUUID', () => global.generateUUID()]]) {
|
|
30
|
+
describe(name, () => {
|
|
31
|
+
it('has the c455 version-4 layout', () => {
|
|
32
|
+
for (let i = 0; i < 200; i++)
|
|
33
|
+
assert.match(generate(), UUID);
|
|
34
|
+
});
|
|
35
|
+
it('does not repeat', () => {
|
|
36
|
+
const seen = new Set();
|
|
37
|
+
for (let i = 0; i < 2000; i++)
|
|
38
|
+
seen.add(generate());
|
|
39
|
+
assert.equal(seen.size, 2000);
|
|
40
|
+
});
|
|
41
|
+
it('mixes the timestamp into the leading digits', () => {
|
|
42
|
+
assert.equal(withFixedEntropy(0, generate), '00865efc-c455-4b81-8000-000000000000');
|
|
43
|
+
assert.equal(withFixedEntropy(9, generate), '991fe785-c455-441a-9999-999999999999');
|
|
44
|
+
assert.equal(withFixedEntropy(15, generate), 'ff754deb-c455-4a70-bfff-ffffffffffff');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|