Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP name encryption #269

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useCallback } from 'react';
import { useSelector } from 'react-redux';
import { getCodebookVariablesForNodeType } from '~/lib/interviewer/selectors/protocol';
import {
Expand All @@ -13,78 +14,96 @@ export const useNodeAttributes = (
node: NcNode,
): {
getById<T extends VariableValue>(attributeId: string): Promise<T | undefined>;
getByName<T extends VariableValue>(
attributeName: string,
): Promise<T | undefined>;
getByName<T extends VariableValue>({
attributeName,
ignoreCase,
}: {
attributeName: string;
ignoreCase?: boolean;
}): Promise<T | undefined>;
} => {
const codebookAttributes = useSelector(
getCodebookVariablesForNodeType(node.type),
);
const nodeAttributes = getEntityAttributes(node);
const { requirePassphrase } = usePassphrase();

const getById = async <T extends VariableValue>(
attributeId: string,
): Promise<T | undefined> => {
const isEncrypted = codebookAttributes[attributeId]?.encrypted;
const getById = useCallback(
async <T extends VariableValue>(
attributeId: string,
): Promise<T | undefined> => {
const isEncrypted = codebookAttributes[attributeId]?.encrypted;

if (!isEncrypted) {
return nodeAttributes[attributeId] as T | undefined;
}
// If the attribute is not encrypted, we can return it directly
if (!isEncrypted) {
return nodeAttributes[attributeId] as T | undefined;
}

const secureAttributes = node[entitySecureAttributesMeta]?.[attributeId];
const secureAttributes = node[entitySecureAttributesMeta]?.[attributeId];

if (!secureAttributes) {
// eslint-disable-next-line no-console
console.log(`Node ${node._uid} is missing secure attributes`);
return undefined;
}
if (!secureAttributes) {
// eslint-disable-next-line no-console
console.log(`Node ${node._uid} is missing secure attributes`);
return undefined;
}

// This will trigger a prompt for the passphrase, and throw an error if it is cancelled.
try {
const passphrase = await requirePassphrase();
// This will trigger a prompt for the passphrase, and throw an error if it is cancelled.
try {
const passphrase = await requirePassphrase();

const decryptedValue = await decryptData(
{
[entitySecureAttributesMeta]: {
salt: secureAttributes.salt,
iv: secureAttributes.iv,
const decryptedValue = await decryptData(
{
[entitySecureAttributesMeta]: {
salt: secureAttributes.salt,
iv: secureAttributes.iv,
},
data: nodeAttributes[attributeId] as number[],
},
data: nodeAttributes[attributeId] as number[],
},
passphrase,
);
passphrase,
);

return decryptedValue as T;
} catch (e) {
// User cancelled or passphrase was incorrect
if (e instanceof UnauthorizedError) {
return decryptedValue as T;
} catch (e) {
console.log('here', e, e instanceof UnauthorizedError);
// User cancelled or passphrase was incorrect
if (e instanceof UnauthorizedError) {
throw e;
}

// Internal error should be logged

// eslint-disable-next-line no-console
console.error(e);
return undefined;
}
},
[node, nodeAttributes, codebookAttributes, requirePassphrase],
);

// Internal error should be logged

// eslint-disable-next-line no-console
console.error(e);
return undefined;
}
};
const getByName = useCallback(
async <T extends VariableValue>({
attributeName,
ignoreCase,
}: {
attributeName: string;
ignoreCase?: boolean;
}): Promise<T | undefined> => {
const attributeId = Object.keys(codebookAttributes).find((id) => {
const name = codebookAttributes[id]!.name;

const getByName = async <T extends VariableValue>(
attributeName: string,
): Promise<T | undefined> => {
const attributeId = Object.keys(codebookAttributes).find(
(id) =>
codebookAttributes[id]!.name.toLowerCase() ===
attributeName.toLowerCase(),
);
return ignoreCase
? name.toLowerCase() === attributeName.toLowerCase()
: name === attributeName;
});

if (!attributeId) {
return undefined;
}
if (!attributeId) {
return undefined;
}

return await getById(attributeId);
};
return await getById(attributeId);
},
[getById, codebookAttributes],
);

return {
getByName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ export function useNodeLabel(node: NcNode) {
async function calculateLabel() {
// 1. Look for a variable called 'name'.
try {
const variableCalledName = await getByName<string>('name');
const variableCalledName = await getByName<string>({
attributeName: 'name',
ignoreCase: true,
});

if (variableCalledName) {
setLabel(variableCalledName);
Expand All @@ -23,6 +26,9 @@ export function useNodeLabel(node: NcNode) {
setLabel('🔒');
return;
}
console.log('yooo');
setLabel('Error fetching name');
return;
}

// 2. Look for a property on the node with a key of ‘name’
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,8 @@ export const usePassphrase = () => {
updateState({ isPrompting: false });

if (!userInput) {
const error = new UnauthorizedError();
window.dispatchEvent(
new CustomEvent('passphrase-cancel', { detail: error }),
);
reject(error);
return;
window.dispatchEvent(new CustomEvent('passphrase-cancel'));
throw new UnauthorizedError();
}

updateState({
Expand Down
47 changes: 46 additions & 1 deletion lib/interviewer/containers/Interfaces/Anonymisation/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { entitySecureAttributesMeta } from '~/lib/shared-consts';
import {
type EntityAttributesProperty,
type EntitySecureAttributesMeta,
entitySecureAttributesMeta,
type NcNode,
} from '~/lib/shared-consts';

export const SESSION_STORAGE_KEY = 'passphrase';

Expand Down Expand Up @@ -92,3 +97,43 @@ export async function decryptData(

return decoder.decode(decryptedData);
}

export async function generateSecureAttributes(
attributes: NcNode[EntityAttributesProperty],
passphrase: string,
): Promise<{
secureAttributes: NcNode[EntitySecureAttributesMeta];
encryptedAttributes: NcNode[EntityAttributesProperty];
}> {
const secureAttributes: NcNode[EntitySecureAttributesMeta] = {};
const encryptedAttributes: NcNode[EntityAttributesProperty] = {};

for (const [key, value] of Object.entries(attributes)) {
// TODO: expand this for other variable types
if (typeof value === 'string') {
const encoder = new TextEncoder();
// Create a new salt and IV for each encryption
const salt = crypto.getRandomValues(new Uint8Array(16));
const iv = crypto.getRandomValues(new Uint8Array(12));

const encryptionKey = await generateKey(passphrase, salt);
const encryptedData = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: iv },
encryptionKey,
encoder.encode(value),
);

secureAttributes[key] = {
iv,
salt,
};

encryptedAttributes[key] = Array.from(new Uint8Array(encryptedData));
}
}

return {
secureAttributes,
encryptedAttributes,
};
}
8 changes: 4 additions & 4 deletions lib/interviewer/ducks/modules/dialogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { type Dispatch } from '@reduxjs/toolkit';
import { type ReactNode } from 'react';
import { v4 as uuid } from 'uuid';

const OPEN_DIALOG = Symbol('PROTOCOL/OPEN_DIALOG');
const CLOSE_DIALOG = Symbol('PROTOCOL/CLOSE_DIALOG');
export const OPEN_DIALOG = Symbol('PROTOCOL/OPEN_DIALOG');
export const CLOSE_DIALOG = Symbol('PROTOCOL/CLOSE_DIALOG');

type OpenDialogAction = {
export type OpenDialogAction = {
type: typeof OPEN_DIALOG;
id: string;
dialog: Omit<Dialog, 'id'>;
Expand Down Expand Up @@ -60,7 +60,7 @@ const initialState = [
// },
] as Dialog[];

const openDialog = (dialog: Dialog) => (dispatch: Dispatch) =>
export const openDialog = (dialog: Dialog) => (dispatch: Dispatch) =>
new Promise((resolve) => {
const onConfirm = () => {
if (dialog.onConfirm) {
Expand Down
Loading
Loading