Skip to content

Commit e919fa1

Browse files
refactor: move oauth tests to embed-sdk repo (#81)
1 parent 35bb030 commit e919fa1

File tree

11 files changed

+1914
-88
lines changed

11 files changed

+1914
-88
lines changed

.github/workflows/build.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,5 @@ jobs:
2727
- run: pnpm install
2828

2929
- run: pnpm run build
30+
31+
- run: pnpm run test

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"lint": "turbo run lint",
1313
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
1414
"typecheck": "turbo run typecheck",
15-
"test": "echo \"Error: no test specified\" && exit 1"
15+
"test": "turbo run test"
1616
},
1717
"author": "sigmacomputing",
1818
"license": "ISC",

packages/embed-sdk/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"lint": "eslint . --ext .ts",
2525
"watch": "tsup --watch",
2626
"typecheck": "tsc --noEmit",
27-
"test": "echo \"Error: no test specified\" && exit 1"
27+
"test": "echo \"Warn: no test specified\""
2828
},
2929
"main": "./dist/index.js",
3030
"module": "./dist/index.mjs",

packages/node-embed-sdk/package.json

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"lint": "eslint . --ext .ts",
2626
"watch": "tsup --watch",
2727
"typecheck": "tsc --noEmit",
28-
"test": "echo \"Error: no test specified\" && exit 1"
28+
"test": "jest"
2929
},
3030
"main": "./dist/index.js",
3131
"module": "./dist/index.mjs",
@@ -46,9 +46,33 @@
4646
"devDependencies": {
4747
"@sigmacomputing/eslint-config": "workspace:*",
4848
"@sigmacomputing/typescript-config": "workspace:*",
49-
"@types/node": "^20.17.16"
49+
"@swc/core": "^1.11.29",
50+
"@swc/jest": "^0.2.38",
51+
"@types/jest": "^29.5.14",
52+
"@types/node": "^20.17.16",
53+
"jest": "^29.7.0"
5054
},
5155
"engines": {
5256
"node": ">=18"
57+
},
58+
"jest": {
59+
"transform": {
60+
"^.+\\.tsx?$": [
61+
"@swc/jest",
62+
{
63+
"module": {
64+
"type": "commonjs"
65+
},
66+
"sourceMaps": "inline"
67+
}
68+
]
69+
},
70+
"testEnvironment": "node",
71+
"testMatch": [
72+
"**/__tests__/**.(i|)(spec|test).(j|t)s(x|)"
73+
],
74+
"testPathIgnorePatterns": [
75+
"<rootDir>/dist/"
76+
]
5377
}
5478
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { decrypt, encrypt } from "../index";
2+
import { _testExports } from "../encryption";
3+
4+
const EMBED_SECRET = "my fake embed secret";
5+
6+
describe("oauth token encryption", () => {
7+
it("can encrypt and decrypt using a passphrase", () => {
8+
const plaintext = "hello, world!";
9+
10+
// Should be able to encrypt and then immediately decrypt.
11+
const encryptedToken = encrypt(EMBED_SECRET, plaintext);
12+
const decryptedToken = decrypt(EMBED_SECRET, encryptedToken);
13+
expect(decryptedToken).toBe(plaintext);
14+
});
15+
16+
function toEncodedString(
17+
salt: string,
18+
iv: string,
19+
tag: string,
20+
ciphertext: string,
21+
): string {
22+
return `${salt}.${iv}.${tag}.${ciphertext}`;
23+
}
24+
25+
it("only throws error when reading an incorrectly encoded string", () => {
26+
// Throws for invalid format.
27+
expect(() => {
28+
_testExports.asEncodedPassphraseEncryptionOutput("hello, world!");
29+
}).toThrow();
30+
31+
// Throws for valid format, but with non-base64 components.
32+
expect(() => {
33+
_testExports.asEncodedPassphraseEncryptionOutput(
34+
toEncodedString("(salt)", "(iv)", "(tag)", "(ciphertext)"),
35+
);
36+
}).toThrow();
37+
38+
// Throws for valid format with base64 components of invalid length.
39+
expect(() => {
40+
_testExports.asEncodedPassphraseEncryptionOutput(
41+
toEncodedString("YQ==", "Yg==", "Yw==", "ZA=="),
42+
);
43+
}).toThrow();
44+
45+
// Does not throw for valid format with base64 components of valid length.
46+
const salt = Buffer.from(
47+
"s".repeat(
48+
_testExports.PBKDF2_HMAC_SHA256_KEY_DERIVATION.SALT_LENGTH_BYTES,
49+
),
50+
).toString("base64");
51+
const iv = Buffer.from(
52+
"i".repeat(_testExports.AES_256_GCM_ENCRYPTION.IV_LENGTH_BYTES),
53+
).toString("base64");
54+
const tag = Buffer.from(
55+
"t".repeat(_testExports.AES_256_GCM_ENCRYPTION.TAG_LENGTH_BYTES),
56+
).toString("base64");
57+
const ciphertext = Buffer.from(
58+
"c".repeat(10 /* arbitrary length */),
59+
).toString("base64");
60+
expect(() => {
61+
_testExports.asEncodedPassphraseEncryptionOutput(
62+
toEncodedString(salt, iv, tag, ciphertext),
63+
);
64+
}).not.toThrow();
65+
});
66+
});

packages/node-embed-sdk/src/encryption.ts

Lines changed: 45 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import crypto from 'node:crypto';
1+
import crypto from "node:crypto";
22

33
/*
44
* Configuration Constants
@@ -16,7 +16,7 @@ import crypto from 'node:crypto';
1616
* The key length is 256 bits since we are using AES-256-GCM.
1717
*/
1818
const PBKDF2_HMAC_SHA256_KEY_DERIVATION = {
19-
DIGEST: 'sha256',
19+
DIGEST: "sha256",
2020
ITERATIONS: 600_000,
2121
KEY_LENGTH_BYTES: 32, // 256 bits
2222
SALT_LENGTH_BYTES: 16, // 128 bits
@@ -30,7 +30,7 @@ const PBKDF2_HMAC_SHA256_KEY_DERIVATION = {
3030
* recommendations.
3131
*/
3232
const AES_256_GCM_ENCRYPTION = {
33-
ALGORITHM: 'aes-256-gcm',
33+
ALGORITHM: "aes-256-gcm",
3434
IV_LENGTH_BYTES: 12, // 96 bits
3535
TAG_LENGTH_BYTES: 16, // 128 bits
3636
} as const;
@@ -157,12 +157,12 @@ function isPassphraseEncryptionOutput(
157157
value: unknown,
158158
): value is PassphraseEncryptionOutput_t {
159159
// The input should be a non-null object
160-
if (!(value && typeof value === 'object')) return false;
160+
if (!(value && typeof value === "object")) return false;
161161
// The object should have these properties
162-
if (!('salt' in value)) return false;
163-
if (!('iv' in value)) return false;
164-
if (!('tag' in value)) return false;
165-
if (!('ciphertext' in value)) return false;
162+
if (!("salt" in value)) return false;
163+
if (!("iv" in value)) return false;
164+
if (!("tag" in value)) return false;
165+
if (!("ciphertext" in value)) return false;
166166
// The properties should be the correct type
167167
if (!isSalt(value.salt)) return false;
168168
if (!isIV(value.iv)) return false;
@@ -174,14 +174,14 @@ function isPassphraseEncryptionOutput(
174174
function isEncodedPassphraseEncryptionOutput(
175175
value: unknown,
176176
): value is EncodedPassphraseEncryptionOutput_t {
177-
if (typeof value !== 'string') return false;
178-
const parts = value.split('.');
177+
if (typeof value !== "string") return false;
178+
const parts = value.split(".");
179179
if (parts.length !== 4) return false;
180180
const [salt, iv, tag, ciphertext] = parts;
181-
if (!isSalt(Buffer.from(salt, 'base64'))) return false;
182-
if (!isIV(Buffer.from(iv, 'base64'))) return false;
183-
if (!isTag(Buffer.from(tag, 'base64'))) return false;
184-
if (!isCiphertext(Buffer.from(ciphertext, 'base64'))) return false;
181+
if (!isSalt(Buffer.from(salt, "base64"))) return false;
182+
if (!isIV(Buffer.from(iv, "base64"))) return false;
183+
if (!isTag(Buffer.from(tag, "base64"))) return false;
184+
if (!isCiphertext(Buffer.from(ciphertext, "base64"))) return false;
185185
return true;
186186
}
187187

@@ -191,49 +191,49 @@ function isEncodedPassphraseEncryptionOutput(
191191

192192
function asPassphrase(value: unknown): Passphrase_t {
193193
if (!isPassphrase(value)) {
194-
throw new Error('Invalid passphrase.');
194+
throw new Error("Invalid passphrase.");
195195
}
196196
return value;
197197
}
198198

199199
function asSalt(value: unknown): Salt_t {
200200
if (!isSalt(value)) {
201-
throw new Error('Invalid salt.');
201+
throw new Error("Invalid salt.");
202202
}
203203
return value;
204204
}
205205

206206
function asTag(value: unknown): Tag_t {
207207
if (!isTag(value)) {
208-
throw new Error('Invalid tag.');
208+
throw new Error("Invalid tag.");
209209
}
210210
return value;
211211
}
212212

213213
function asIV(value: unknown): IV_t {
214214
if (!isIV(value)) {
215-
throw new Error('Invalid IV.');
215+
throw new Error("Invalid IV.");
216216
}
217217
return value;
218218
}
219219

220220
function asCiphertext(value: unknown): Ciphertext_t {
221221
if (!isCiphertext(value)) {
222-
throw new Error('Invalid ciphertext.');
222+
throw new Error("Invalid ciphertext.");
223223
}
224224
return value;
225225
}
226226

227227
function asPlaintext(value: unknown): Plaintext_t {
228228
if (!isPlaintext(value)) {
229-
throw new Error('Invalid plaintext.');
229+
throw new Error("Invalid plaintext.");
230230
}
231231
return value;
232232
}
233233

234234
function asSymmetricKey(value: unknown): SymmetricKey_t {
235235
if (!isSymmetricKey(value)) {
236-
throw new Error('Invalid symmetric key.');
236+
throw new Error("Invalid symmetric key.");
237237
}
238238
return value;
239239
}
@@ -242,7 +242,7 @@ function asPassphraseEncryptionOutput(
242242
value: unknown,
243243
): PassphraseEncryptionOutput_t {
244244
if (!isPassphraseEncryptionOutput(value)) {
245-
throw new Error('Invalid encryption output.');
245+
throw new Error("Invalid encryption output.");
246246
}
247247
return value;
248248
}
@@ -257,7 +257,7 @@ function asEncodedPassphraseEncryptionOutput(
257257
value: unknown,
258258
): EncodedPassphraseEncryptionOutput_t {
259259
if (!isEncodedPassphraseEncryptionOutput(value)) {
260-
throw new Error('Invalid encoded encryption output.');
260+
throw new Error("Invalid encoded encryption output.");
261261
}
262262
return value;
263263
}
@@ -373,25 +373,25 @@ function encodeEncryptedToken(
373373
tag: Tag_t,
374374
ciphertext: Ciphertext_t,
375375
): string {
376-
const encodedSalt = salt.toString('base64');
377-
const encodedIV = iv.toString('base64');
378-
const encodedTag = tag.toString('base64');
379-
const encodedCiphertext = ciphertext.toString('base64');
376+
const encodedSalt = salt.toString("base64");
377+
const encodedIV = iv.toString("base64");
378+
const encodedTag = tag.toString("base64");
379+
const encodedCiphertext = ciphertext.toString("base64");
380380
return `${encodedSalt}.${encodedIV}.${encodedTag}.${encodedCiphertext}`;
381381
}
382382

383383
function decodeEncryptedToken(
384384
encodedToken: EncodedPassphraseEncryptionOutput_t,
385385
): PassphraseEncryptionOutput_t {
386-
const parts = encodedToken.split('.');
386+
const parts = encodedToken.split(".");
387387
if (parts.length !== 4) {
388-
throw new Error('Expected 4 components in encoded token.');
388+
throw new Error("Expected 4 components in encoded token.");
389389
}
390390
const [encodedSalt, encodedIV, encodedTag, encodedCiphertext] = parts;
391-
const salt = asSalt(Buffer.from(encodedSalt, 'base64'));
392-
const iv = asIV(Buffer.from(encodedIV, 'base64'));
393-
const tag = asTag(Buffer.from(encodedTag, 'base64'));
394-
const ciphertext = asCiphertext(Buffer.from(encodedCiphertext, 'base64'));
391+
const salt = asSalt(Buffer.from(encodedSalt, "base64"));
392+
const iv = asIV(Buffer.from(encodedIV, "base64"));
393+
const tag = asTag(Buffer.from(encodedTag, "base64"));
394+
const ciphertext = asCiphertext(Buffer.from(encodedCiphertext, "base64"));
395395
return { salt, iv, tag, ciphertext };
396396
}
397397

@@ -425,12 +425,9 @@ function decodeEncryptionOutput(
425425
* @param oauthToken the OAuth token to encrypt
426426
* @returns the encrypted token, encoded as a string
427427
*/
428-
export function encrypt(
429-
embedSecret: string,
430-
oauthToken: string,
431-
): string {
432-
const passphrase = asPassphrase(Buffer.from(embedSecret, 'utf8'));
433-
const plaintext = asPlaintext(Buffer.from(oauthToken, 'utf8'));
428+
export function encrypt(embedSecret: string, oauthToken: string): string {
429+
const passphrase = asPassphrase(Buffer.from(embedSecret, "utf8"));
430+
const plaintext = asPlaintext(Buffer.from(oauthToken, "utf8"));
434431
const encryptionOutput = encryptWithPassphrase(passphrase, plaintext);
435432
return encodeEncryptionOutput(encryptionOutput);
436433
}
@@ -442,11 +439,8 @@ export function encrypt(
442439
* @param encryptedToken the encrypted OAuth token to decrypt
443440
* @returns the decrypted token
444441
*/
445-
export function decrypt(
446-
embedSecret: string,
447-
encryptedToken: string,
448-
): string {
449-
const passphrase = asPassphrase(Buffer.from(embedSecret, 'utf8'));
442+
export function decrypt(embedSecret: string, encryptedToken: string): string {
443+
const passphrase = asPassphrase(Buffer.from(embedSecret, "utf8"));
450444
const encryptionOutput = decodeEncryptionOutput(
451445
asEncodedPassphraseEncryptionOutput(encryptedToken),
452446
);
@@ -457,5 +451,11 @@ export function decrypt(
457451
encryptionOutput.tag,
458452
encryptionOutput.ciphertext,
459453
);
460-
return plaintext.toString('utf8');
454+
return plaintext.toString("utf8");
461455
}
456+
457+
export const _testExports = {
458+
PBKDF2_HMAC_SHA256_KEY_DERIVATION,
459+
AES_256_GCM_ENCRYPTION,
460+
asEncodedPassphraseEncryptionOutput,
461+
};
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export * from './encryption';
1+
export { encrypt, decrypt } from "./encryption";

packages/node-embed-sdk/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"forceConsistentCasingInFileNames": true,
1010
"lib": ["ES2022"],
1111
"outDir": "./dist",
12-
"types": ["node"]
12+
"types": ["node", "jest"]
1313
},
1414
"include": ["src"],
1515
"exclude": ["node_modules", "dist"]

packages/react-embed-sdk/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"build": "tsup",
2525
"dev": "tsup --watch",
2626
"lint": "eslint . --ext .ts,.tsx",
27-
"test": "echo \"Error: no test specified\" && exit 1"
27+
"test": "echo \"Warn: no test specified\""
2828
},
2929
"main": "./dist/index.js",
3030
"module": "./dist/index.mjs",

0 commit comments

Comments
 (0)