Skip to content

Commit

Permalink
fix: Fix the token access procedure, use hashed private key as the id…
Browse files Browse the repository at this point in the history
…entifier
  • Loading branch information
wsy19961129 committed Sep 4, 2024
1 parent b995750 commit cb7d96f
Show file tree
Hide file tree
Showing 16 changed files with 163 additions and 133 deletions.
6 changes: 3 additions & 3 deletions packages/itmat-apis/src/graphql/resolvers/studyResolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,9 @@ export class StudyResolvers {
return files.map(el => {
return {
...el,
fileSize: el.fileSize.toString(),
uploadTime: el.life.createdTime.toString(),
uploadedBy: el.life.createdUser
fileSize: el.fileSize?.toString(),
uploadTime: el.life?.createdTime.toString(),
uploadedBy: el.life?.createdUser
};
});
} catch (e) {
Expand Down
12 changes: 6 additions & 6 deletions packages/itmat-apis/src/trpc/userProcedure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,10 +267,10 @@ export class UserRouter {
*/
registerPubkey: this.baseProcedure.input(z.object({
pubkey: z.string(),
signature: z.string(),
hashedPrivateKey: z.string(),
associatedUserId: z.string()
})).mutation(async (opts) => {
return await this.userCore.registerPubkey(opts.ctx.user, opts.input.pubkey, opts.input.signature, opts.input.associatedUserId);
return await this.userCore.registerPubkey(opts.ctx.user, opts.input.pubkey, opts.input.hashedPrivateKey, opts.input.associatedUserId);
}),
/**
* Request an access token.
Expand All @@ -281,9 +281,9 @@ export class UserRouter {
*/
requestAccessToken: this.baseProcedure.input(z.object({
username: z.string(),
pubkey: z.string()
hashedPrivateKey: z.string()
})).mutation(async (opts) => {
return await this.userCore.requestAccessToken(opts.input.username, opts.input.pubkey);
return await this.userCore.requestAccessToken(opts.input.username, opts.input.hashedPrivateKey);
}),
/**
* Get an access token.
Expand All @@ -294,10 +294,10 @@ export class UserRouter {
*/
getAccessToken: this.baseProcedure.input(z.object({
username: z.string(),
pubkey: z.string(),
hashedPrivateKey: z.string(),
signature: z.string()
})).mutation(async (opts) => {
return await this.userCore.getAccessToken(opts.input.username, opts.input.pubkey, opts.input.signature);
return await this.userCore.getAccessToken(opts.input.username, opts.input.hashedPrivateKey, opts.input.signature);
}),
/**
* Issue an access token.
Expand Down
46 changes: 33 additions & 13 deletions packages/itmat-cores/src/coreFunc/dataCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1044,11 +1044,19 @@ export class DataCore {
return [];
}
const batchSize = 10000; // Define a suitable batch size
const promises: Promise<IFile[]>[] = [];
const promises: Promise<Partial<IFile>[]>[] = [];

for (let i = 0; i < fileDataRecords.length; i += batchSize) {
const batchIds = fileDataRecords.slice(i, i + batchSize).map(el => String(el.value));
const promise = this.db.collections.files_collection.find({ id: { $in: batchIds } }, { allowDiskUse: true }).toArray();
const promise = this.db.collections.files_collection.find({ id: { $in: batchIds } }, { allowDiskUse: true })
.project({
'_id': 0,
'fileType': 0,
'fileCategory': 0,
'sharedUsers': 0,
'life.deletedTime': 0,
'life.deletedUser': 0
}).toArray();
promises.push(promise);
}

Expand All @@ -1057,8 +1065,10 @@ export class DataCore {
const users = await this.db.collections.users_collection.find({}).toArray();
const edited = [...files];
for (const file of edited) {
const user = users.find(el => el.id === file.life.createdUser);
file.life.createdUser = user ? `${user.firstname} ${user.lastname}` : file.life.createdUser;
const user = users.find(el => el.id === file.life?.createdUser);
if (file.life) {
file.life.createdUser = user ? `${user.firstname} ${user.lastname}` : file.life?.createdUser;
}
}
return edited;
} else {
Expand Down Expand Up @@ -1182,11 +1192,19 @@ export class DataCore {
return [];
}
const batchSize = 10000; // Define a suitable batch size
const promises: Promise<IFile[]>[] = [];
const promises: Promise<Partial<IFile>[]>[] = [];

for (let i = 0; i < fileDataRecords.length; i += batchSize) {
const batchIds = fileDataRecords.slice(i, i + batchSize).map(el => String(el.value));
const promise = this.db.collections.files_collection.find({ id: { $in: batchIds } }, { allowDiskUse: true }).toArray();
const promise = this.db.collections.files_collection.find({ id: { $in: batchIds } }, { allowDiskUse: true })
.project({
'_id': 0,
'fileType': 0,
'fileCategory': 0,
'sharedUsers': 0,
'life.deletedTime': 0,
'life.deletedUser': 0
}).toArray();
promises.push(promise);
}

Expand All @@ -1195,8 +1213,10 @@ export class DataCore {
const users = await this.db.collections.users_collection.find({}).toArray();
const edited = [...files];
for (const file of edited) {
const user = users.find(el => el.id === file.life.createdUser);
file.life.createdUser = user ? `${user.firstname} ${user.lastname}` : file.life.createdUser;
const user = users.find(el => el.id === file.life?.createdUser);
if (file.life) {
file.life.createdUser = user ? `${user.firstname} ${user.lastname}` : file.life?.createdUser;
}
}
return edited;
} else {
Expand Down Expand Up @@ -1295,11 +1315,12 @@ export class DataCore {
propertyFilter[`${property.name}`] = `$properties.${property.name}`;
}
}

const data = await this.db.collections.data_collection.aggregate<IData>([{
$match: { ...matchFilter, fieldId: fieldId }
}, {
$match: { $or: roleArr }
$match: {
...matchFilter,
fieldId: fieldId,
$or: roleArr
}
}, {
$sort: {
'life.createdTime': -1
Expand All @@ -1321,7 +1342,6 @@ export class DataCore {
metadata: 0 // Exclude the metadata field
}
}], { allowDiskUse: true }).toArray();

return data;
}

Expand Down
36 changes: 7 additions & 29 deletions packages/itmat-cores/src/coreFunc/userCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -673,7 +673,7 @@ export class UserCore {
*
* @return IPubkey - The object of ther registered key.
*/
public async registerPubkey(requester: IUserWithoutToken | undefined, pubkey: string, signature: string | undefined, associatedUserId: string): Promise<IPubkey> {
public async registerPubkey(requester: IUserWithoutToken | undefined, pubkey: string, hashedPrivateKey: string | undefined, associatedUserId: string): Promise<IPubkey> {
if (!requester) {
throw new CoreError(
enumCoreErrors.NOT_LOGGED_IN,
Expand Down Expand Up @@ -711,6 +711,7 @@ export class UserCore {
const entry: IPubkey = {
id: uuid(),
pubkey: pubkey,
hashedPrivateKey: hashedPrivateKey,
associatedUserId: associatedUserId,
jwtPubkey: keypair.publicKey,
jwtSeckey: keypair.privateKey,
Expand All @@ -727,27 +728,6 @@ export class UserCore {
};

await this.db.collections.pubkeys_collection.insertOne(entry);

await this.mailer.sendMail({
from: `${this.config.appName} <${this.config.nodemailer.auth.user}>`,
to: user.email,
subject: `[${this.config.appName}] Public-key Registration!`,
html: `
<p>
Dear ${user.firstname},
<p>
<p>
You have successfully registered your public-key "${pubkey}" on ${this.config.appName}!<br/>
You will need to keep your private key secretly. <br/>
You will also need to sign a message (using your public-key) to authenticate the owner of the public key. <br/>
</p>
<br/>
<p>
The ${this.config.appName} Team.
</p>
`
});
return entry;
}
/**
Expand All @@ -770,16 +750,15 @@ export class UserCore {
* @param pubkeyKey - The public key.
* @returns - The challenge.
*/
public async requestAccessToken(username: string, pubkeyKey: string) {
public async requestAccessToken(username: string, hashedPrivateKey: string) {
const user = await this.db.collections.users_collection.findOne({ 'username': username, 'life.deletedTime': null });
if (!user) {
throw new CoreError(
enumCoreErrors.CLIENT_MALFORMED_INPUT,
'This public-key has not been registered yet.'
);
}

const pubkeyrec = await this.db.collections.pubkeys_collection.findOne({ 'associatedUserId': user.id, 'pubkey': pubkeyKey, 'life.deletedTime': null });
const pubkeyrec = await this.db.collections.pubkeys_collection.findOne({ 'associatedUserId': user.id, 'hashedPrivateKey': hashedPrivateKey, 'life.deletedTime': null });

if (!pubkeyrec) {
throw new CoreError(
Expand All @@ -802,15 +781,15 @@ export class UserCore {
* @param life - The life of the token.
* @returns - The token.
*/
public async getAccessToken(username: string, pubkeyKey: string, signature: string, life = 12000) {
public async getAccessToken(username: string, hashedPrivateKey: string, signature: string, life = 12000) {
const user = await this.db.collections.users_collection.findOne({ 'username': username, 'life.deletedTime': null });
if (!user) {
throw new CoreError(
enumCoreErrors.CLIENT_MALFORMED_INPUT,
'This public-key has not been registered yet.'
);
}
const pubkeyrec = await this.db.collections.pubkeys_collection.findOne({ 'associatedUserId': user.id, 'pubkey': pubkeyKey, 'life.deletedTime': null });
const pubkeyrec = await this.db.collections.pubkeys_collection.findOne({ 'associatedUserId': user.id, 'hashedPrivateKey': hashedPrivateKey, 'life.deletedTime': null });
if (!pubkeyrec) {
throw new CoreError(
enumCoreErrors.CLIENT_MALFORMED_INPUT,
Expand All @@ -824,15 +803,14 @@ export class UserCore {
'Please request a challenge first.'
);
}

const isVerified = crypto.verify(
null, // No hash algorithm because the challenge is already hashed
Buffer.from(pubkeyrec.challenge, 'hex'), // Convert the hex-encoded challenge to binary
{
key: pubkeyrec.pubkey,
padding: crypto.constants.RSA_PKCS1_PSS_PADDING
},
Buffer.from(signature, 'base64') // Decode the Base64-encoded signature
Buffer.from(signature, 'hex') // Decode the Base64-encoded signature
);

if (!isVerified) {
Expand Down
2 changes: 1 addition & 1 deletion packages/itmat-cores/src/webdav/dmpWebDAV.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,7 @@ export class DMPFileSystem extends webdav.FileSystem {
} else {
try {
const files = await this.dataCore.getStudyFiles(user, study.id);
callback(undefined, files.map(el => el.fileName));
callback(undefined, files.map(el => el.fileName ?? ''));
return;
} catch {
callback(new Error('Failed to get study files.'));
Expand Down
1 change: 1 addition & 0 deletions packages/itmat-types/src/types/pubkey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { IBase } from './base';

export interface IPubkey extends IBase {
pubkey: string;
hashedPrivateKey?: string;
jwtPubkey: string;
jwtSeckey: string;
refreshCounter: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,8 +310,8 @@ export const FileBlock: FunctionComponent<{ user: IUserWithoutToken, fields: IFi
} else {
const keyword = searchedKeyword.toLowerCase();
if (
el.fileName.toLowerCase().includes(keyword) ||
Object.keys(el.properties).some(key => String(el.properties[key]).toLowerCase().includes(keyword))
el.fileName?.toLowerCase().includes(keyword) ||
Object.keys(el.properties ?? {}).some(key => String(el.properties?.[key]).toLowerCase().includes(keyword))
) {
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const PickDatasetSection: FunctionComponent<{ datasets: IStudy[] }> = ({ dataset
rowKey={(rec) => rec.id}
pagination={false}
columns={columns}
dataSource={datasets}
dataSource={datasets.sort((a, b) => a.name.localeCompare(b.name))}
size='small'
/>
<br /><br />
Expand Down
2 changes: 1 addition & 1 deletion packages/itmat-ui-react/src/components/log/logList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ const generateLogColumns = (users: IUserWithoutToken[], barThreshold: number[])
return record.status;
}
}, {
title: 'Time Consumed/ms',
title: 'Time Elapsed/ms',
dataIndex: 'timeConsumed',
key: 'timeConsumed',
sorter: (a, b) => a.timeConsumed - b.timeConsumed,
Expand Down
5 changes: 1 addition & 4 deletions packages/itmat-ui-react/src/components/profile/index.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import { FunctionComponent } from 'react';
import { ProfileManagementSection } from './profile';
import css from './profile.module.css';
import { Divider } from 'antd';
import { MyKeys } from './keys';
import {MyWebauthn} from './webauthn';
import { MyWebauthn } from './webauthn';

export const ProfilePage: FunctionComponent = () => {
return (
<div className={css.page_container}>
<ProfileManagementSection />
<Divider />
<MyKeys />
<Divider />
<MyWebauthn />
</div>
);
Expand Down
36 changes: 28 additions & 8 deletions packages/itmat-ui-react/src/components/profile/keys.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,20 @@ import css from './profile.module.css';
import { trpc } from '../../utils/trpc';
import copy from 'copy-to-clipboard';
import { Key } from '../../utils/dmpCrypto/dmp.key';
import { useQueryClient } from '@tanstack/react-query';
import { IPubkey } from '@itmat-broker/itmat-types';

export const MyKeys: FunctionComponent = () => {
const whoAmI = trpc.user.whoAmI.useQuery();
const getUserKeys = trpc.user.getUserKeys.useQuery({ userId: whoAmI.data.id });
const queryClient = useQueryClient();
const deletePubkey = trpc.user.deletePubkey.useMutation({
onSuccess: () => {
onSuccess: (data) => {
void message.success('Key deleted.');
const queryKey = [['user', 'getUserKeys'], { input: { userId: whoAmI.data.id }, type: 'query' }];
const cache: IPubkey[] = queryClient.getQueryData(queryKey) ?? [];
const newCache = cache.filter((el) => el.id !== data.id);
queryClient.setQueryData(queryKey, newCache);
},
onError: () => {
void message.error('Failed to delete this key.');
Expand Down Expand Up @@ -115,17 +122,20 @@ const KeyGeneration: React.FunctionComponent<{ userId: string }> = ({ userId })
const [isKeyGenOpen, setIsKeyGenOpen] = useState(false);
const [completedKeypairGen, setcompletedKeypairGen] = useState(false);
const [exportedKeyPair, setExportedKeyPair] = useState({ privateKey: '', publicKey: '' });
const queryClient = useQueryClient();
const registerPubkey = trpc.user.registerPubkey.useMutation({
onSuccess: () => {
onSuccess: (data) => {
void message.success('Key registered.');
const queryKey = [['user', 'getUserKeys'], { input: { userId: userId }, type: 'query' }];
const cache: IPubkey[] = queryClient.getQueryData(queryKey) ?? [];
const newCache = [...cache, data];
queryClient.setQueryData(queryKey, newCache);
},
onError: () => {
void message.error('Failed to register this key.');
}
});

const [signature, setSignature] = useState('');

const [downloadLink, setDownloadLink] = useState('');
// function for generating file and set download link
const makeTextFile = (filecontent: string) => {
Expand All @@ -136,6 +146,19 @@ const KeyGeneration: React.FunctionComponent<{ userId: string }> = ({ userId })
setDownloadLink(window.URL.createObjectURL(data));
};

const hashedPrivateKey = async (privateKey) => {
const encoder = new TextEncoder();
const privateKeyBuffer = encoder.encode(privateKey);

// Compute the SHA-256 hash
const hashBuffer = await crypto.subtle.digest('SHA-256', privateKeyBuffer);

// Convert the hash to a hex string
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(byte => byte.toString(16).padStart(2, '0')).join('');
return hashHex;
};

return (
<div>
<Button type='primary' onClick={() => setIsKeyGenOpen(true)}>Generate a Key Pair</Button>
Expand All @@ -147,7 +170,7 @@ const KeyGeneration: React.FunctionComponent<{ userId: string }> = ({ userId })
void (async () => {
await registerPubkey.mutate({
pubkey: exportedKeyPair.publicKey,
signature: signature,
hashedPrivateKey: await hashedPrivateKey(exportedKeyPair.privateKey),
associatedUserId: userId
});
})();
Expand All @@ -163,10 +186,7 @@ const KeyGeneration: React.FunctionComponent<{ userId: string }> = ({ userId })
const keyPair = await cryptoInBrowser.keyGen();
const exportedKeyPair = await Key.exportRSAKey(keyPair);
setExportedKeyPair(exportedKeyPair);
const message = exportedKeyPair.publicKey;
const signature = await cryptoInBrowser.signGen(message, keyPair.privateKey);
setcompletedKeypairGen(true);
setSignature(signature);
})();
}}>
Do not have public/private keypair? Generate one (In-browser)!
Expand Down
Loading

0 comments on commit cb7d96f

Please sign in to comment.