oidc-spa 7.1.8 → 7.1.9
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/core/AuthResponse.d.ts +0 -1
- package/core/AuthResponse.js +0 -7
- package/core/AuthResponse.js.map +1 -1
- package/core/createOidc.js +1 -1
- package/core/handleOidcCallback.js +9 -4
- package/core/handleOidcCallback.js.map +1 -1
- package/core/iframeMessageProtection.d.ts +20 -0
- package/core/iframeMessageProtection.js +135 -0
- package/core/iframeMessageProtection.js.map +1 -0
- package/core/loginSilent.js +141 -118
- package/core/loginSilent.js.map +1 -1
- package/entrypoint.d.ts +1 -0
- package/entrypoint.js +14 -1
- package/entrypoint.js.map +1 -1
- package/package.json +9 -1
- package/src/core/AuthResponse.ts +0 -9
- package/src/core/handleOidcCallback.ts +6 -4
- package/src/core/iframeMessageProtection.ts +99 -0
- package/src/core/loginSilent.ts +20 -4
- package/src/entrypoint.ts +25 -2
- package/src/tools/asymmetricEncryption.ts +184 -0
- package/tools/asymmetricEncryption.d.ts +18 -0
- package/tools/asymmetricEncryption.js +181 -0
- package/tools/asymmetricEncryption.js.map +1 -0
package/entrypoint.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entrypoint.js","sourceRoot":"","sources":["src/entrypoint.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"entrypoint.js","sourceRoot":"","sources":["src/entrypoint.ts"],"names":[],"mappings":";;AAMA,sCA8DC;AApED,gEAGmC;AACnC,0EAAqG;AAErG,SAAgB,aAAa,CAAC,MAM7B;IACS,IAAA,KAAiE,MAAM,aAAN,MAAM,cAAN,MAAM,GAAI,EAAE,EAA3E,WAAW,iBAAA,EAAE,oBAAoB,0BAAA,EAAE,uBAAuB,EAAvB,eAAe,mBAAG,KAAK,KAAiB,CAAC;IAE5E,IAAA,SAAS,GAAK,IAAA,uCAAkB,GAAE,UAAzB,CAA0B;IAE3C,IAAM,aAAa,GAAG,CAAC,SAAS,CAAC;IAEjC,IAAI,aAAa,EAAE,CAAC;QAChB,IAAA,uEAAkD,GAAE,CAAC;QAErD,IAAI,oBAAoB,EAAE,CAAC;YACvB,IAAM,sBAAsB,GAAG,UAAU,CAAC,cAAc,CAAC;YAEzD,MAAM,CAAC,MAAM,CAAC,sBAAsB,CAAC,SAAS,CAAC,CAAC;YAChD,MAAM,CAAC,MAAM,CAAC,sBAAsB,CAAC,CAAC;YAEtC,MAAM,CAAC,cAAc,CAAC,UAAU,EAAE,gBAAgB,EAAE;gBAChD,YAAY,EAAE,KAAK;gBACnB,QAAQ,EAAE,KAAK;gBACf,UAAU,EAAE,IAAI;gBAChB,KAAK,EAAE,sBAAsB;aAChC,CAAC,CAAC;QACP,CAAC;QAED,IAAI,WAAW,EAAE,CAAC;YACd,IAAM,aAAa,GAAG,UAAU,CAAC,KAAK,CAAC;YAEvC,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC;YACvC,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;YAE7B,MAAM,CAAC,cAAc,CAAC,UAAU,EAAE,OAAO,EAAE;gBACvC,YAAY,EAAE,KAAK;gBACnB,QAAQ,EAAE,KAAK;gBACf,UAAU,EAAE,IAAI;gBAChB,KAAK,EAAE,aAAa;aACvB,CAAC,CAAC;QACP,CAAC;QAED,IAAI,eAAe,EAAE,CAAC;YAClB,IAAM,iBAAiB,GAAG,UAAU,CAAC,SAAS,CAAC;YAE/C,MAAM,CAAC,MAAM,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC;YAC3C,MAAM,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC;YAEjC,MAAM,CAAC,cAAc,CAAC,UAAU,EAAE,WAAW,EAAE;gBAC3C,YAAY,EAAE,KAAK;gBACnB,QAAQ,EAAE,KAAK;gBACf,UAAU,EAAE,IAAI;gBAChB,KAAK,EAAE,iBAAiB;aAC3B,CAAC,CAAC;QACP,CAAC;QAED,IAAA,6EAAmD,GAAE,CAAC;IAC1D,CAAC;IAED,OAAO,EAAE,aAAa,eAAA,EAAE,CAAC;AAC7B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oidc-spa",
|
|
3
|
-
"version": "7.1.
|
|
3
|
+
"version": "7.1.9",
|
|
4
4
|
"description": "Openidconnect client for Single Page Applications",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"./src/core/createOidc.ts",
|
|
42
42
|
"./src/core/evtIsUserActive.ts",
|
|
43
43
|
"./src/core/handleOidcCallback.ts",
|
|
44
|
+
"./src/core/iframeMessageProtection.ts",
|
|
44
45
|
"./src/core/index.ts",
|
|
45
46
|
"./src/core/initialLocationHref.ts",
|
|
46
47
|
"./src/core/isNewBrowserSession.ts",
|
|
@@ -64,6 +65,7 @@
|
|
|
64
65
|
"./src/tools/Evt.ts",
|
|
65
66
|
"./src/tools/StatefulEvt.ts",
|
|
66
67
|
"./src/tools/ValueOrAsyncGetter.ts",
|
|
68
|
+
"./src/tools/asymmetricEncryption.ts",
|
|
67
69
|
"./src/tools/base64.ts",
|
|
68
70
|
"./src/tools/createObjectThatThrowsIfAccessed.ts",
|
|
69
71
|
"./src/tools/decodeJwt.ts",
|
|
@@ -122,6 +124,9 @@
|
|
|
122
124
|
"./core/handleOidcCallback.d.ts",
|
|
123
125
|
"./core/handleOidcCallback.js",
|
|
124
126
|
"./core/handleOidcCallback.js.map",
|
|
127
|
+
"./core/iframeMessageProtection.d.ts",
|
|
128
|
+
"./core/iframeMessageProtection.js",
|
|
129
|
+
"./core/iframeMessageProtection.js.map",
|
|
125
130
|
"./core/index.d.ts",
|
|
126
131
|
"./core/index.js",
|
|
127
132
|
"./core/index.js.map",
|
|
@@ -191,6 +196,9 @@
|
|
|
191
196
|
"./tools/ValueOrAsyncGetter.d.ts",
|
|
192
197
|
"./tools/ValueOrAsyncGetter.js",
|
|
193
198
|
"./tools/ValueOrAsyncGetter.js.map",
|
|
199
|
+
"./tools/asymmetricEncryption.d.ts",
|
|
200
|
+
"./tools/asymmetricEncryption.js",
|
|
201
|
+
"./tools/asymmetricEncryption.js.map",
|
|
194
202
|
"./tools/base64.d.ts",
|
|
195
203
|
"./tools/base64.js",
|
|
196
204
|
"./tools/base64.js.map",
|
package/src/core/AuthResponse.ts
CHANGED
|
@@ -5,15 +5,6 @@ export type AuthResponse = {
|
|
|
5
5
|
[key: string]: string | undefined;
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
-
export function getIsAuthResponse(data: any): data is AuthResponse {
|
|
9
|
-
return (
|
|
10
|
-
data instanceof Object &&
|
|
11
|
-
"state" in data &&
|
|
12
|
-
typeof data.state === "string" &&
|
|
13
|
-
Object.values(data).every(value => value === undefined || typeof value === "string")
|
|
14
|
-
);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
8
|
export function authResponseToUrl(authResponse: AuthResponse): string {
|
|
18
9
|
let authResponseUrl = "https://dummy.com";
|
|
19
10
|
|
|
@@ -8,6 +8,7 @@ import { assert, id } from "../vendor/frontend/tsafe";
|
|
|
8
8
|
import type { AuthResponse } from "./AuthResponse";
|
|
9
9
|
import { initialLocationHref } from "./initialLocationHref";
|
|
10
10
|
import { captureFetch } from "./trustedFetch";
|
|
11
|
+
import { encryptAuthResponse } from "./iframeMessageProtection";
|
|
11
12
|
|
|
12
13
|
captureFetch();
|
|
13
14
|
|
|
@@ -111,7 +112,8 @@ function handleOidcCallback_nonMemoized(): { isHandled: boolean } {
|
|
|
111
112
|
// NOTE: This is a "better than nothing" approach.
|
|
112
113
|
// Under some circumstances it's possible to get stuck on this url
|
|
113
114
|
// if there is no "next" page in the history for example, navigating
|
|
114
|
-
// forward is a NoOp. So in that case it's better to
|
|
115
|
+
// forward is a NoOp. So in that case it's better to reload the same route
|
|
116
|
+
// with just the authResponse removed from the url to avoid re-entering here.
|
|
115
117
|
setTimeout(() => {
|
|
116
118
|
const { protocol, host, pathname, hash } = window.location;
|
|
117
119
|
window.location.href = `${protocol}//${host}${pathname}${hash}`;
|
|
@@ -131,9 +133,9 @@ function handleOidcCallback_nonMemoized(): { isHandled: boolean } {
|
|
|
131
133
|
|
|
132
134
|
switch (stateData.context) {
|
|
133
135
|
case "iframe":
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
},
|
|
136
|
+
encryptAuthResponse({
|
|
137
|
+
authResponse
|
|
138
|
+
}).then(({ encryptedMessage }) => parent.postMessage(encryptedMessage, location.origin));
|
|
137
139
|
break;
|
|
138
140
|
case "redirect":
|
|
139
141
|
markStateDataAsProcessedByCallback({ stateQueryParamValue });
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { assert } from "tsafe/assert";
|
|
2
|
+
import { asymmetricEncrypt, asymmetricDecrypt, generateKeys } from "../tools/asymmetricEncryption";
|
|
3
|
+
import { type AuthResponse } from "./AuthResponse";
|
|
4
|
+
|
|
5
|
+
const setItem_real = Storage.prototype.setItem;
|
|
6
|
+
|
|
7
|
+
const SESSION_STORAGE_PREFIX = "oidc-spa_iframe_authResponse_publicKey_";
|
|
8
|
+
|
|
9
|
+
export function preventSessionStorageSetItemOfPublicKeyByThirdParty() {
|
|
10
|
+
const setItem_protected = function setItem(this: any, key: string, value: string): void {
|
|
11
|
+
if (this !== sessionStorage) {
|
|
12
|
+
return setItem_real.call(this, key, value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (key.startsWith(SESSION_STORAGE_PREFIX)) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
"Attack prevented by oidc-spa. You have malicious code running in your system"
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return setItem_real.call(sessionStorage, key, value);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
{
|
|
25
|
+
const pd = Object.getOwnPropertyDescriptor(Storage.prototype, "setItem");
|
|
26
|
+
|
|
27
|
+
assert(pd !== undefined);
|
|
28
|
+
|
|
29
|
+
Object.defineProperty(Storage.prototype, "setItem", {
|
|
30
|
+
enumerable: pd.enumerable,
|
|
31
|
+
writable: pd.writable,
|
|
32
|
+
value: setItem_protected
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const ENCRYPTED_AUTH_RESPONSES_PREFIX = "oidc-spa_encrypted_authResponse_";
|
|
38
|
+
|
|
39
|
+
function getSessionStorageKey(params: { stateQueryParamValue: string }) {
|
|
40
|
+
const { stateQueryParamValue } = params;
|
|
41
|
+
|
|
42
|
+
return `${SESSION_STORAGE_PREFIX}${stateQueryParamValue}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function initIframeMessageProtection(params: { stateQueryParamValue: string }) {
|
|
46
|
+
const { stateQueryParamValue } = params;
|
|
47
|
+
|
|
48
|
+
const { publicKey, privateKey } = await generateKeys();
|
|
49
|
+
|
|
50
|
+
const sessionStorageKey = getSessionStorageKey({ stateQueryParamValue });
|
|
51
|
+
|
|
52
|
+
setItem_real.call(sessionStorage, sessionStorageKey, publicKey);
|
|
53
|
+
|
|
54
|
+
function getIsEncryptedAuthResponse(params: { message: unknown }): boolean {
|
|
55
|
+
const { message } = params;
|
|
56
|
+
|
|
57
|
+
return typeof message === "string" && message.startsWith(ENCRYPTED_AUTH_RESPONSES_PREFIX);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function decodeEncryptedAuth(params: {
|
|
61
|
+
encryptedAuthResponse: string;
|
|
62
|
+
}): Promise<{ authResponse: AuthResponse }> {
|
|
63
|
+
const { encryptedAuthResponse } = params;
|
|
64
|
+
|
|
65
|
+
const { message: authResponse_str } = await asymmetricDecrypt({
|
|
66
|
+
encryptedMessage: encryptedAuthResponse.slice(ENCRYPTED_AUTH_RESPONSES_PREFIX.length),
|
|
67
|
+
privateKey
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const authResponse: AuthResponse = JSON.parse(authResponse_str);
|
|
71
|
+
|
|
72
|
+
return { authResponse };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function clearSessionStoragePublicKey() {
|
|
76
|
+
sessionStorage.removeItem(sessionStorageKey);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { getIsEncryptedAuthResponse, decodeEncryptedAuth, clearSessionStoragePublicKey };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function encryptAuthResponse(params: { authResponse: AuthResponse }) {
|
|
83
|
+
const { authResponse } = params;
|
|
84
|
+
|
|
85
|
+
const publicKey = sessionStorage.getItem(
|
|
86
|
+
getSessionStorageKey({ stateQueryParamValue: authResponse.state })
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
assert(publicKey !== null, "2293302");
|
|
90
|
+
|
|
91
|
+
const { encryptedMessage: encryptedMessage_withoutPrefix } = await asymmetricEncrypt({
|
|
92
|
+
publicKey,
|
|
93
|
+
message: JSON.stringify(authResponse)
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const encryptedMessage = `${ENCRYPTED_AUTH_RESPONSES_PREFIX}${encryptedMessage_withoutPrefix}`;
|
|
97
|
+
|
|
98
|
+
return { encryptedMessage };
|
|
99
|
+
}
|
package/src/core/loginSilent.ts
CHANGED
|
@@ -5,8 +5,9 @@ import { getStateData, clearStateStore, type StateData } from "./StateData";
|
|
|
5
5
|
import { getDownlinkAndRtt } from "../tools/getDownlinkAndRtt";
|
|
6
6
|
import { getIsDev } from "../tools/isDev";
|
|
7
7
|
import type { User as OidcClientTsUser } from "../vendor/frontend/oidc-client-ts-and-jwt-decode";
|
|
8
|
-
import { type AuthResponse
|
|
8
|
+
import { type AuthResponse } from "./AuthResponse";
|
|
9
9
|
import { addOrUpdateSearchParam } from "../tools/urlSearchParams";
|
|
10
|
+
import { initIframeMessageProtection } from "./iframeMessageProtection";
|
|
10
11
|
|
|
11
12
|
type ResultOfLoginSilent =
|
|
12
13
|
| {
|
|
@@ -74,6 +75,11 @@ export async function loginSilent(params: {
|
|
|
74
75
|
return Math.max(BASE_DELAY_MS, dynamicDelay);
|
|
75
76
|
})();
|
|
76
77
|
|
|
78
|
+
const { decodeEncryptedAuth, getIsEncryptedAuthResponse, clearSessionStoragePublicKey } =
|
|
79
|
+
await initIframeMessageProtection({
|
|
80
|
+
stateQueryParamValue: stateQueryParamValue_instance
|
|
81
|
+
});
|
|
82
|
+
|
|
77
83
|
const timeout = setTimeout(async () => {
|
|
78
84
|
dResult.resolve({
|
|
79
85
|
outcome: "failure",
|
|
@@ -81,12 +87,20 @@ export async function loginSilent(params: {
|
|
|
81
87
|
});
|
|
82
88
|
}, timeoutDelayMs);
|
|
83
89
|
|
|
84
|
-
const listener = (event: MessageEvent) => {
|
|
85
|
-
if (
|
|
90
|
+
const listener = async (event: MessageEvent) => {
|
|
91
|
+
if (event.origin !== window.location.origin) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (
|
|
96
|
+
!getIsEncryptedAuthResponse({
|
|
97
|
+
message: event.data
|
|
98
|
+
})
|
|
99
|
+
) {
|
|
86
100
|
return;
|
|
87
101
|
}
|
|
88
102
|
|
|
89
|
-
const authResponse = event.data;
|
|
103
|
+
const { authResponse } = await decodeEncryptedAuth({ encryptedAuthResponse: event.data });
|
|
90
104
|
|
|
91
105
|
const stateData = getStateData({ stateQueryParamValue: authResponse.state });
|
|
92
106
|
|
|
@@ -181,6 +195,8 @@ export async function loginSilent(params: {
|
|
|
181
195
|
);
|
|
182
196
|
|
|
183
197
|
dResult.pr.then(result => {
|
|
198
|
+
clearSessionStoragePublicKey();
|
|
199
|
+
|
|
184
200
|
if (result.outcome === "failure") {
|
|
185
201
|
clearStateStore({ stateQueryParamValue: stateQueryParamValue_instance });
|
|
186
202
|
}
|
package/src/entrypoint.ts
CHANGED
|
@@ -2,9 +2,16 @@ import {
|
|
|
2
2
|
handleOidcCallback,
|
|
3
3
|
moveRedirectAuthResponseFromSessionStorageToMemory
|
|
4
4
|
} from "./core/handleOidcCallback";
|
|
5
|
+
import { preventSessionStorageSetItemOfPublicKeyByThirdParty } from "./core/iframeMessageProtection";
|
|
5
6
|
|
|
6
|
-
export function oidcEarlyInit(params: {
|
|
7
|
-
|
|
7
|
+
export function oidcEarlyInit(params: {
|
|
8
|
+
freezeFetch: boolean;
|
|
9
|
+
freezeXMLHttpRequest: boolean;
|
|
10
|
+
// NOTE: Made optional just to avoid breaking change.
|
|
11
|
+
// Will be made mandatory next major.
|
|
12
|
+
freezeWebSocket?: boolean;
|
|
13
|
+
}) {
|
|
14
|
+
const { freezeFetch, freezeXMLHttpRequest, freezeWebSocket = false } = params ?? {};
|
|
8
15
|
|
|
9
16
|
const { isHandled } = handleOidcCallback();
|
|
10
17
|
|
|
@@ -40,6 +47,22 @@ export function oidcEarlyInit(params: { freezeFetch: boolean; freezeXMLHttpReque
|
|
|
40
47
|
value: fetch_trusted
|
|
41
48
|
});
|
|
42
49
|
}
|
|
50
|
+
|
|
51
|
+
if (freezeWebSocket) {
|
|
52
|
+
const WebSocket_trusted = globalThis.WebSocket;
|
|
53
|
+
|
|
54
|
+
Object.freeze(WebSocket_trusted.prototype);
|
|
55
|
+
Object.freeze(WebSocket_trusted);
|
|
56
|
+
|
|
57
|
+
Object.defineProperty(globalThis, "WebSocket", {
|
|
58
|
+
configurable: false,
|
|
59
|
+
writable: false,
|
|
60
|
+
enumerable: true,
|
|
61
|
+
value: WebSocket_trusted
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
preventSessionStorageSetItemOfPublicKeyByThirdParty();
|
|
43
66
|
}
|
|
44
67
|
|
|
45
68
|
return { shouldLoadApp };
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
type AsymmetricKeys = {
|
|
2
|
+
publicKey: string; // base64-encoded JSON export of CryptoKey
|
|
3
|
+
privateKey: string; // base64-encoded JSON export of CryptoKey
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
const INFO_LABEL = "oidc-spa/tools/asymmetricEncryption";
|
|
7
|
+
|
|
8
|
+
export async function generateKeys(): Promise<AsymmetricKeys> {
|
|
9
|
+
const keyPair = await crypto.subtle.generateKey(
|
|
10
|
+
{
|
|
11
|
+
name: "ECDH",
|
|
12
|
+
namedCurve: "P-256"
|
|
13
|
+
},
|
|
14
|
+
true,
|
|
15
|
+
["deriveKey", "deriveBits"]
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const publicKeyRaw = await crypto.subtle.exportKey("jwk", keyPair.publicKey);
|
|
19
|
+
const privateKeyRaw = await crypto.subtle.exportKey("jwk", keyPair.privateKey);
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
publicKey: btoa(JSON.stringify(publicKeyRaw)),
|
|
23
|
+
privateKey: btoa(JSON.stringify(privateKeyRaw))
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function asymmetricEncrypt(params: {
|
|
28
|
+
publicKey: string;
|
|
29
|
+
message: string;
|
|
30
|
+
}): Promise<{ encryptedMessage: string }> {
|
|
31
|
+
const { publicKey, message } = params;
|
|
32
|
+
|
|
33
|
+
const importedPublicKey = await crypto.subtle.importKey(
|
|
34
|
+
"jwk",
|
|
35
|
+
JSON.parse(atob(publicKey)),
|
|
36
|
+
{
|
|
37
|
+
name: "ECDH",
|
|
38
|
+
namedCurve: "P-256"
|
|
39
|
+
},
|
|
40
|
+
false,
|
|
41
|
+
[]
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const ephemeralKeyPair = await crypto.subtle.generateKey(
|
|
45
|
+
{
|
|
46
|
+
name: "ECDH",
|
|
47
|
+
namedCurve: "P-256"
|
|
48
|
+
},
|
|
49
|
+
true,
|
|
50
|
+
["deriveKey", "deriveBits"]
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const sharedSecret = await crypto.subtle.deriveBits(
|
|
54
|
+
{
|
|
55
|
+
name: "ECDH",
|
|
56
|
+
public: importedPublicKey
|
|
57
|
+
},
|
|
58
|
+
ephemeralKeyPair.privateKey,
|
|
59
|
+
256
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
63
|
+
const infoBytes = new TextEncoder().encode(INFO_LABEL);
|
|
64
|
+
|
|
65
|
+
const hkdfKey = await crypto.subtle.importKey("raw", sharedSecret, "HKDF", false, ["deriveKey"]);
|
|
66
|
+
|
|
67
|
+
const derivedKey = await crypto.subtle.deriveKey(
|
|
68
|
+
{
|
|
69
|
+
name: "HKDF",
|
|
70
|
+
hash: "SHA-256",
|
|
71
|
+
salt,
|
|
72
|
+
info: infoBytes
|
|
73
|
+
},
|
|
74
|
+
hkdfKey,
|
|
75
|
+
{ name: "AES-GCM", length: 256 },
|
|
76
|
+
false,
|
|
77
|
+
["encrypt"]
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
81
|
+
const encodedMessage = new TextEncoder().encode(message);
|
|
82
|
+
|
|
83
|
+
const ciphertext = await crypto.subtle.encrypt(
|
|
84
|
+
{
|
|
85
|
+
name: "AES-GCM",
|
|
86
|
+
iv
|
|
87
|
+
},
|
|
88
|
+
derivedKey,
|
|
89
|
+
encodedMessage
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const ephemeralPubKeyRaw = await crypto.subtle.exportKey("jwk", ephemeralKeyPair.publicKey);
|
|
93
|
+
|
|
94
|
+
const payload = {
|
|
95
|
+
ephemeralPubKey: ephemeralPubKeyRaw,
|
|
96
|
+
iv: Array.from(iv),
|
|
97
|
+
salt: Array.from(salt),
|
|
98
|
+
ciphertext: Array.from(new Uint8Array(ciphertext))
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
encryptedMessage: btoa(JSON.stringify(payload))
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function asymmetricDecrypt(params: {
|
|
107
|
+
privateKey: string;
|
|
108
|
+
encryptedMessage: string;
|
|
109
|
+
}): Promise<{ message: string }> {
|
|
110
|
+
const { privateKey, encryptedMessage } = params;
|
|
111
|
+
|
|
112
|
+
const {
|
|
113
|
+
ephemeralPubKey,
|
|
114
|
+
iv,
|
|
115
|
+
salt,
|
|
116
|
+
ciphertext
|
|
117
|
+
}: {
|
|
118
|
+
ephemeralPubKey: JsonWebKey;
|
|
119
|
+
iv: number[];
|
|
120
|
+
salt: number[];
|
|
121
|
+
ciphertext: number[];
|
|
122
|
+
} = JSON.parse(atob(encryptedMessage));
|
|
123
|
+
|
|
124
|
+
const importedPrivateKey = await crypto.subtle.importKey(
|
|
125
|
+
"jwk",
|
|
126
|
+
JSON.parse(atob(privateKey)),
|
|
127
|
+
{
|
|
128
|
+
name: "ECDH",
|
|
129
|
+
namedCurve: "P-256"
|
|
130
|
+
},
|
|
131
|
+
false,
|
|
132
|
+
["deriveKey", "deriveBits"]
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const importedEphemeralPubKey = await crypto.subtle.importKey(
|
|
136
|
+
"jwk",
|
|
137
|
+
ephemeralPubKey,
|
|
138
|
+
{
|
|
139
|
+
name: "ECDH",
|
|
140
|
+
namedCurve: "P-256"
|
|
141
|
+
},
|
|
142
|
+
false,
|
|
143
|
+
[]
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const sharedSecret = await crypto.subtle.deriveBits(
|
|
147
|
+
{
|
|
148
|
+
name: "ECDH",
|
|
149
|
+
public: importedEphemeralPubKey
|
|
150
|
+
},
|
|
151
|
+
importedPrivateKey,
|
|
152
|
+
256
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const infoBytes = new TextEncoder().encode(INFO_LABEL);
|
|
156
|
+
|
|
157
|
+
const hkdfKey = await crypto.subtle.importKey("raw", sharedSecret, "HKDF", false, ["deriveKey"]);
|
|
158
|
+
|
|
159
|
+
const derivedKey = await crypto.subtle.deriveKey(
|
|
160
|
+
{
|
|
161
|
+
name: "HKDF",
|
|
162
|
+
hash: "SHA-256",
|
|
163
|
+
salt: new Uint8Array(salt),
|
|
164
|
+
info: infoBytes
|
|
165
|
+
},
|
|
166
|
+
hkdfKey,
|
|
167
|
+
{ name: "AES-GCM", length: 256 },
|
|
168
|
+
false,
|
|
169
|
+
["decrypt"]
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const decryptedBuffer = await crypto.subtle.decrypt(
|
|
173
|
+
{
|
|
174
|
+
name: "AES-GCM",
|
|
175
|
+
iv: new Uint8Array(iv)
|
|
176
|
+
},
|
|
177
|
+
derivedKey,
|
|
178
|
+
new Uint8Array(ciphertext)
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
message: new TextDecoder().decode(decryptedBuffer)
|
|
183
|
+
};
|
|
184
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
type AsymmetricKeys = {
|
|
2
|
+
publicKey: string;
|
|
3
|
+
privateKey: string;
|
|
4
|
+
};
|
|
5
|
+
export declare function generateKeys(): Promise<AsymmetricKeys>;
|
|
6
|
+
export declare function asymmetricEncrypt(params: {
|
|
7
|
+
publicKey: string;
|
|
8
|
+
message: string;
|
|
9
|
+
}): Promise<{
|
|
10
|
+
encryptedMessage: string;
|
|
11
|
+
}>;
|
|
12
|
+
export declare function asymmetricDecrypt(params: {
|
|
13
|
+
privateKey: string;
|
|
14
|
+
encryptedMessage: string;
|
|
15
|
+
}): Promise<{
|
|
16
|
+
message: string;
|
|
17
|
+
}>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
12
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
|
13
|
+
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
14
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
15
|
+
function step(op) {
|
|
16
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
17
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
18
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
19
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
20
|
+
switch (op[0]) {
|
|
21
|
+
case 0: case 1: t = op; break;
|
|
22
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
23
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
24
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
25
|
+
default:
|
|
26
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
27
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
28
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
29
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
30
|
+
if (t[2]) _.ops.pop();
|
|
31
|
+
_.trys.pop(); continue;
|
|
32
|
+
}
|
|
33
|
+
op = body.call(thisArg, _);
|
|
34
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
35
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.generateKeys = generateKeys;
|
|
40
|
+
exports.asymmetricEncrypt = asymmetricEncrypt;
|
|
41
|
+
exports.asymmetricDecrypt = asymmetricDecrypt;
|
|
42
|
+
var INFO_LABEL = "oidc-spa/tools/asymmetricEncryption";
|
|
43
|
+
function generateKeys() {
|
|
44
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
45
|
+
var keyPair, publicKeyRaw, privateKeyRaw;
|
|
46
|
+
return __generator(this, function (_a) {
|
|
47
|
+
switch (_a.label) {
|
|
48
|
+
case 0: return [4 /*yield*/, crypto.subtle.generateKey({
|
|
49
|
+
name: "ECDH",
|
|
50
|
+
namedCurve: "P-256"
|
|
51
|
+
}, true, ["deriveKey", "deriveBits"])];
|
|
52
|
+
case 1:
|
|
53
|
+
keyPair = _a.sent();
|
|
54
|
+
return [4 /*yield*/, crypto.subtle.exportKey("jwk", keyPair.publicKey)];
|
|
55
|
+
case 2:
|
|
56
|
+
publicKeyRaw = _a.sent();
|
|
57
|
+
return [4 /*yield*/, crypto.subtle.exportKey("jwk", keyPair.privateKey)];
|
|
58
|
+
case 3:
|
|
59
|
+
privateKeyRaw = _a.sent();
|
|
60
|
+
return [2 /*return*/, {
|
|
61
|
+
publicKey: btoa(JSON.stringify(publicKeyRaw)),
|
|
62
|
+
privateKey: btoa(JSON.stringify(privateKeyRaw))
|
|
63
|
+
}];
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function asymmetricEncrypt(params) {
|
|
69
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
70
|
+
var publicKey, message, importedPublicKey, ephemeralKeyPair, sharedSecret, salt, infoBytes, hkdfKey, derivedKey, iv, encodedMessage, ciphertext, ephemeralPubKeyRaw, payload;
|
|
71
|
+
return __generator(this, function (_a) {
|
|
72
|
+
switch (_a.label) {
|
|
73
|
+
case 0:
|
|
74
|
+
publicKey = params.publicKey, message = params.message;
|
|
75
|
+
return [4 /*yield*/, crypto.subtle.importKey("jwk", JSON.parse(atob(publicKey)), {
|
|
76
|
+
name: "ECDH",
|
|
77
|
+
namedCurve: "P-256"
|
|
78
|
+
}, false, [])];
|
|
79
|
+
case 1:
|
|
80
|
+
importedPublicKey = _a.sent();
|
|
81
|
+
return [4 /*yield*/, crypto.subtle.generateKey({
|
|
82
|
+
name: "ECDH",
|
|
83
|
+
namedCurve: "P-256"
|
|
84
|
+
}, true, ["deriveKey", "deriveBits"])];
|
|
85
|
+
case 2:
|
|
86
|
+
ephemeralKeyPair = _a.sent();
|
|
87
|
+
return [4 /*yield*/, crypto.subtle.deriveBits({
|
|
88
|
+
name: "ECDH",
|
|
89
|
+
public: importedPublicKey
|
|
90
|
+
}, ephemeralKeyPair.privateKey, 256)];
|
|
91
|
+
case 3:
|
|
92
|
+
sharedSecret = _a.sent();
|
|
93
|
+
salt = crypto.getRandomValues(new Uint8Array(16));
|
|
94
|
+
infoBytes = new TextEncoder().encode(INFO_LABEL);
|
|
95
|
+
return [4 /*yield*/, crypto.subtle.importKey("raw", sharedSecret, "HKDF", false, ["deriveKey"])];
|
|
96
|
+
case 4:
|
|
97
|
+
hkdfKey = _a.sent();
|
|
98
|
+
return [4 /*yield*/, crypto.subtle.deriveKey({
|
|
99
|
+
name: "HKDF",
|
|
100
|
+
hash: "SHA-256",
|
|
101
|
+
salt: salt,
|
|
102
|
+
info: infoBytes
|
|
103
|
+
}, hkdfKey, { name: "AES-GCM", length: 256 }, false, ["encrypt"])];
|
|
104
|
+
case 5:
|
|
105
|
+
derivedKey = _a.sent();
|
|
106
|
+
iv = crypto.getRandomValues(new Uint8Array(12));
|
|
107
|
+
encodedMessage = new TextEncoder().encode(message);
|
|
108
|
+
return [4 /*yield*/, crypto.subtle.encrypt({
|
|
109
|
+
name: "AES-GCM",
|
|
110
|
+
iv: iv
|
|
111
|
+
}, derivedKey, encodedMessage)];
|
|
112
|
+
case 6:
|
|
113
|
+
ciphertext = _a.sent();
|
|
114
|
+
return [4 /*yield*/, crypto.subtle.exportKey("jwk", ephemeralKeyPair.publicKey)];
|
|
115
|
+
case 7:
|
|
116
|
+
ephemeralPubKeyRaw = _a.sent();
|
|
117
|
+
payload = {
|
|
118
|
+
ephemeralPubKey: ephemeralPubKeyRaw,
|
|
119
|
+
iv: Array.from(iv),
|
|
120
|
+
salt: Array.from(salt),
|
|
121
|
+
ciphertext: Array.from(new Uint8Array(ciphertext))
|
|
122
|
+
};
|
|
123
|
+
return [2 /*return*/, {
|
|
124
|
+
encryptedMessage: btoa(JSON.stringify(payload))
|
|
125
|
+
}];
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
function asymmetricDecrypt(params) {
|
|
131
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
132
|
+
var privateKey, encryptedMessage, _a, ephemeralPubKey, iv, salt, ciphertext, importedPrivateKey, importedEphemeralPubKey, sharedSecret, infoBytes, hkdfKey, derivedKey, decryptedBuffer;
|
|
133
|
+
return __generator(this, function (_b) {
|
|
134
|
+
switch (_b.label) {
|
|
135
|
+
case 0:
|
|
136
|
+
privateKey = params.privateKey, encryptedMessage = params.encryptedMessage;
|
|
137
|
+
_a = JSON.parse(atob(encryptedMessage)), ephemeralPubKey = _a.ephemeralPubKey, iv = _a.iv, salt = _a.salt, ciphertext = _a.ciphertext;
|
|
138
|
+
return [4 /*yield*/, crypto.subtle.importKey("jwk", JSON.parse(atob(privateKey)), {
|
|
139
|
+
name: "ECDH",
|
|
140
|
+
namedCurve: "P-256"
|
|
141
|
+
}, false, ["deriveKey", "deriveBits"])];
|
|
142
|
+
case 1:
|
|
143
|
+
importedPrivateKey = _b.sent();
|
|
144
|
+
return [4 /*yield*/, crypto.subtle.importKey("jwk", ephemeralPubKey, {
|
|
145
|
+
name: "ECDH",
|
|
146
|
+
namedCurve: "P-256"
|
|
147
|
+
}, false, [])];
|
|
148
|
+
case 2:
|
|
149
|
+
importedEphemeralPubKey = _b.sent();
|
|
150
|
+
return [4 /*yield*/, crypto.subtle.deriveBits({
|
|
151
|
+
name: "ECDH",
|
|
152
|
+
public: importedEphemeralPubKey
|
|
153
|
+
}, importedPrivateKey, 256)];
|
|
154
|
+
case 3:
|
|
155
|
+
sharedSecret = _b.sent();
|
|
156
|
+
infoBytes = new TextEncoder().encode(INFO_LABEL);
|
|
157
|
+
return [4 /*yield*/, crypto.subtle.importKey("raw", sharedSecret, "HKDF", false, ["deriveKey"])];
|
|
158
|
+
case 4:
|
|
159
|
+
hkdfKey = _b.sent();
|
|
160
|
+
return [4 /*yield*/, crypto.subtle.deriveKey({
|
|
161
|
+
name: "HKDF",
|
|
162
|
+
hash: "SHA-256",
|
|
163
|
+
salt: new Uint8Array(salt),
|
|
164
|
+
info: infoBytes
|
|
165
|
+
}, hkdfKey, { name: "AES-GCM", length: 256 }, false, ["decrypt"])];
|
|
166
|
+
case 5:
|
|
167
|
+
derivedKey = _b.sent();
|
|
168
|
+
return [4 /*yield*/, crypto.subtle.decrypt({
|
|
169
|
+
name: "AES-GCM",
|
|
170
|
+
iv: new Uint8Array(iv)
|
|
171
|
+
}, derivedKey, new Uint8Array(ciphertext))];
|
|
172
|
+
case 6:
|
|
173
|
+
decryptedBuffer = _b.sent();
|
|
174
|
+
return [2 /*return*/, {
|
|
175
|
+
message: new TextDecoder().decode(decryptedBuffer)
|
|
176
|
+
}];
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
//# sourceMappingURL=asymmetricEncryption.js.map
|