Skip to content

Commit

Permalink
Fix crypto cookie decrypt
Browse files Browse the repository at this point in the history
  • Loading branch information
ggodlewski committed Feb 7, 2025
1 parent 8083b5d commit d8e138f
Show file tree
Hide file tree
Showing 5 changed files with 202 additions and 30 deletions.
1 change: 1 addition & 0 deletions .github/workflows/DevelopServerDeploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ jobs:
- name: Start
run: |
docker run -d --name wikigdrive-develop \
--cpu-period=100000 --cpu-quota=400000 --memory=4096m \
--restart unless-stopped \
--network nginx \
--tmpfs /tmp \
Expand Down
20 changes: 10 additions & 10 deletions src/containers/server/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,31 +63,31 @@ interface JwtDecryptedPayload extends GoogleUser {
driveId: string;
}

function signToken(payload: JwtDecryptedPayload, jwtSecret: string): string {
async function signToken(payload: JwtDecryptedPayload, jwtSecret: string): Promise<string> {
const expiresIn = 365 * 24 * 3600;

const encrypted: JwtEncryptedPayload = {
sub: payload.id,
name: payload.name,
email: payload.email,
gat: encrypt(payload.google_access_token, jwtSecret),
grt: payload.google_refresh_token ? encrypt(payload.google_refresh_token, jwtSecret) : undefined,
gat: await encrypt(payload.google_access_token, jwtSecret),
grt: payload.google_refresh_token ? await encrypt(payload.google_refresh_token, jwtSecret) : undefined,
ged: payload.google_expiry_date,
driveId: payload.driveId
};

return jsonwebtoken.sign(encrypted, jwtSecret, { expiresIn });
}

function verifyToken(accessCookie: string, jwtSecret: string): JwtDecryptedPayload {
async function verifyToken(accessCookie: string, jwtSecret: string): Promise<JwtDecryptedPayload> {
const encrypted: JwtEncryptedPayload = <JwtEncryptedPayload>jsonwebtoken.verify(accessCookie, jwtSecret);

return {
id: encrypted.sub,
name: encrypted.name,
email: encrypted.email,
google_access_token: decrypt(encrypted.gat, process.env.JWT_SECRET),
google_refresh_token: encrypted.grt ? decrypt(encrypted.grt, process.env.JWT_SECRET) : undefined,
google_access_token: await decrypt(encrypted.gat, process.env.JWT_SECRET),
google_refresh_token: encrypted.grt ? await decrypt(encrypted.grt, process.env.JWT_SECRET) : undefined,
google_expiry_date: encrypted.ged,
driveId: encrypted.driveId
};
Expand Down Expand Up @@ -247,7 +247,7 @@ export async function getAuth(req: Request, res: Response, next) {
if (driveId) {
const drive = await googleDriveService.getDrive(await authClient.getAccessToken(), driveId);
if (drive.id) {
const accessToken = signToken({
const accessToken = await signToken({
...googleUser,
...await authClient.getAuthData(),
driveId: driveId
Expand All @@ -257,7 +257,7 @@ export async function getAuth(req: Request, res: Response, next) {
return;
}
} else {
const accessToken = signToken({
const accessToken = await signToken({
...googleUser,
...await authClient.getAuthData(),
driveId: driveId
Expand Down Expand Up @@ -296,7 +296,7 @@ async function decodeAuthenticateInfo(req, res, next) {
const jwtSecret = process.env.JWT_SECRET;

try {
const decoded = verifyToken(req.cookies.accessToken, jwtSecret);
const decoded = await verifyToken(req.cookies.accessToken, jwtSecret);
if (!decoded.id) {
return next(redirError(req, 'No jwt.sub'));
}
Expand All @@ -321,7 +321,7 @@ async function decodeAuthenticateInfo(req, res, next) {
return next(redirError(req, 'Unauthorized to read drive: ' + driveId));
}

const accessToken: string = signToken({
const accessToken: string = await signToken({
...decoded,
...await authClient.getAuthData(),
driveId: driveId
Expand Down
53 changes: 36 additions & 17 deletions src/google/GoogleAuthService.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,47 @@
import crypto from 'node:crypto';
import {Buffer} from 'node:buffer';
import * as base64 from './base64.ts';

// https://stackoverflow.com/questions/19641783/google-drive-api-username-password-authentication#19643080
// https://developers.google.com/identity/protocols/OAuth2ServiceAccount
const IV = '5383165476e1c2e3';
export function encrypt(val: string, key: string) {
key = Buffer.from(key).toString('hex').substring(0, 32);
const cipher = crypto.createCipheriv('aes-256-cbc', key, IV);
const encrypted = cipher.update(val, 'utf8', 'hex');
const final = encrypted + cipher.final('hex');
const buffer = Buffer.from(final, 'hex');
return buffer.toString('base64');

const IV: ArrayBuffer = new TextEncoder().encode('5383165476e1c2e3').slice(0, 16);

export async function encrypt(val: string, keyString: string) {
const key = await crypto.subtle.importKey(
'raw', new TextEncoder().encode(keyString).slice(0, 16), { //this is the algorithm options
name: 'AES-CBC',
},
false,
['encrypt', 'decrypt']
);

const encrypted = await crypto.subtle.encrypt({
name: 'AES-CBC',
iv: IV, // @TODO: Don't re-use initialization vectors! Always generate a new iv every time your encrypt!
}, key, new TextEncoder().encode(val));

return base64.fromUint8Array(new Uint8Array(encrypted));
}

export function decrypt(encrypted: string, key: string) {
export async function decrypt(encrypted: string, keyString: string) {
if (!encrypted) {
return null;
}
key = Buffer.from(key).toString('hex').substring(0, 32);
const buffer = Buffer.from(encrypted, 'base64');
const decipher = crypto.createDecipheriv('aes-256-cbc', key, IV);
const decrypted = decipher.update(buffer.toString('hex'), 'hex', 'utf8');
const f = decrypted + decipher.final('utf8');
return f;

const key = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(keyString).slice(0, 16), { //this is the algorithm options
name: 'AES-CBC',
},
false,
['encrypt', 'decrypt']
);

const decrypted = await crypto.subtle.decrypt({
name: 'AES-CBC',
iv: IV,
}, key, base64.toUint8Array(encrypted));

return new TextDecoder().decode(decrypted);
}

export interface TokenInfo {
Expand Down
152 changes: 152 additions & 0 deletions src/google/base64.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
function getLengths(b64: string): [number, number] {
const len: number = b64.length;

// if (len % 4 > 0) {
// throw new TypeError("Invalid string. Length must be a multiple of 4");
// }

// Trim off extra bytes after placeholder bytes are found
// See: https://github.com/beatgammit/base64-js/issues/42
let validLen: number = b64.indexOf('=');

if (validLen === -1) {
validLen = len;
}

const placeHoldersLen: number = validLen === len ? 0 : 4 - (validLen % 4);

return [validLen, placeHoldersLen];
}

function init(
lookup: string[],
revLookup: number[],
urlsafe: boolean = false,
) {
function _byteLength(validLen: number, placeHoldersLen: number): number {
return Math.floor(((validLen + placeHoldersLen) * 3) / 4 - placeHoldersLen);
}

function tripletToBase64(num: number): string {
return (
lookup[(num >> 18) & 0x3f] +
lookup[(num >> 12) & 0x3f] +
lookup[(num >> 6) & 0x3f] +
lookup[num & 0x3f]
);
}

function encodeChunk(buf: Uint8Array, start: number, end: number): string {
const out: string[] = new Array((end - start) / 3);

for (let i: number = start, curTriplet: number = 0; i < end; i += 3) {
out[curTriplet++] = tripletToBase64(
(buf[i] << 16) + (buf[i + 1] << 8) + buf[i + 2],
);
}

return out.join('');
}

return {
// base64 is 4/3 + up to two characters of the original data
byteLength(b64: string): number {
return _byteLength.apply(null, getLengths(b64));

Check failure on line 54 in src/google/base64.ts

View workflow job for this annotation

GitHub Actions / test

Use the spread operator instead of '.apply()'
},
toUint8Array(b64: string): Uint8Array {
const [validLen, placeHoldersLen]: number[] = getLengths(b64);

const buf = new Uint8Array(_byteLength(validLen, placeHoldersLen));

// If there are placeholders, only get up to the last complete 4 chars
const len: number = placeHoldersLen ? validLen - 4 : validLen;

let tmp: number;
let curByte: number = 0;
let i: number;

for (i = 0; i < len; i += 4) {
tmp = (revLookup[b64.charCodeAt(i)] << 18) |
(revLookup[b64.charCodeAt(i + 1)] << 12) |
(revLookup[b64.charCodeAt(i + 2)] << 6) |
revLookup[b64.charCodeAt(i + 3)];
buf[curByte++] = (tmp >> 16) & 0xff;
buf[curByte++] = (tmp >> 8) & 0xff;
buf[curByte++] = tmp & 0xff;
}

if (placeHoldersLen === 2) {
tmp = (revLookup[b64.charCodeAt(i)] << 2) |
(revLookup[b64.charCodeAt(i + 1)] >> 4);
buf[curByte++] = tmp & 0xff;
} else if (placeHoldersLen === 1) {
tmp = (revLookup[b64.charCodeAt(i)] << 10) |
(revLookup[b64.charCodeAt(i + 1)] << 4) |
(revLookup[b64.charCodeAt(i + 2)] >> 2);
buf[curByte++] = (tmp >> 8) & 0xff;
buf[curByte++] = tmp & 0xff;
}

return buf;
},
fromUint8Array(buf: Uint8Array): string {
const maxChunkLength: number = 16383; // Must be multiple of 3

const len: number = buf.length;

const extraBytes: number = len % 3; // If we have 1 byte left, pad 2 bytes

const len2: number = len - extraBytes;

const parts: string[] = new Array(
Math.ceil(len2 / maxChunkLength) + (extraBytes ? 1 : 0),
);

let curChunk: number = 0;
let chunkEnd: number;

// Go through the array every three bytes, we'll deal with trailing stuff later
for (let i: number = 0; i < len2; i += maxChunkLength) {
chunkEnd = i + maxChunkLength;
parts[curChunk++] = encodeChunk(
buf,
i,
chunkEnd > len2 ? len2 : chunkEnd,
);
}

let tmp: number;

// Pad the end with zeros, but make sure to not forget the extra bytes
if (extraBytes === 1) {
tmp = buf[len2];
parts[curChunk] = lookup[tmp >> 2] + lookup[(tmp << 4) & 0x3f];
if (!urlsafe) parts[curChunk] += '==';
} else if (extraBytes === 2) {
tmp = (buf[len2] << 8) | (buf[len2 + 1] & 0xff);
parts[curChunk] = lookup[tmp >> 10] +
lookup[(tmp >> 4) & 0x3f] +
lookup[(tmp << 2) & 0x3f];
if (!urlsafe) parts[curChunk] += '=';
}

return parts.join('');
},
};
}

const lookup: string[] = [];
const revLookup: number[] = [];
const code: string =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';

for (let i: number = 0, l = code.length; i < l; ++i) {
lookup[i] = code[i];
revLookup[code.charCodeAt(i)] = i;
}

export const { byteLength, toUint8Array, fromUint8Array } = init(
lookup,
revLookup,
true,
);
6 changes: 3 additions & 3 deletions test/encryptTest.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import test from './tester.ts';
import {decrypt, encrypt} from '../src/google/GoogleAuthService.ts';

test('test encryption', (t) => {
test('test encryption', async (t) => {
const dec = 'plaintext123plaintext123plaintext123plaintext123plaintext123plaintext123plaintext123plaintext123plaintext123plaintext123plaintext123plaintext123';
const enc = encrypt(dec, 'sikretsikretsikretsikretsikret');
const dec2 = decrypt(enc, 'sikretsikretsikretsikretsikret');
const enc = await encrypt(dec, 'sikretsikretsikretsikretsikret');
const dec2 = await decrypt(enc, 'sikretsikretsikretsikretsikret');
t.is(dec, dec2);
});

0 comments on commit d8e138f

Please sign in to comment.