How to do end-to-end encryption with web crypto? Do I need a salt?

84 Views Asked by At

In my application the users can sync their data via dropbox and similar options. So my idea was to provide users with a simple kind of end to end encryption using web crypto. The user then enters the same password on each device he intends to sync with. So there is no classic database or server endpoint involved in the process and there is no way to exchange the salt between the different instances.

As I am not an expert on the subject I am struggling with the question on how to deal with the password salt. For now I decided to use the password to generate the salt, which is often discouraged. Since the salted password is only saved on the users machine, I am wondering if it is alright to do it this way.

Here is my code:

const ALGORITHM = 'AES-GCM' as const;

const base642ab = (base64: string): ArrayBuffer => {
  const binary_string = window.atob(base64);
  const len = binary_string.length;
  const bytes = new Uint8Array(len);
  for (let i = 0; i < len; i++) {
    bytes[i] = binary_string.charCodeAt(i);
  }
  return bytes.buffer;
};

const ab2base64 = (buffer: ArrayBuffer): string => {
  const binary = Array.prototype.map
    .call(new Uint8Array(buffer), (byte: number) => String.fromCharCode(byte))
    .join('');
  return window.btoa(binary);
};

const generateKey = async (password: string): Promise<CryptoKey> => {
  const enc = new TextEncoder();
  const passwordBuffer = enc.encode(password);
  const ops = {
    name: 'PBKDF2',
    // TODO this is probably not very secure
    // on the other hand: the salt is used for saving the password securely, so maybe it is not imporant
    // for our specific use case? We would need to need some security expert input on this
    salt: enc.encode(password),
    iterations: 1000,
    hash: 'SHA-256',
  };
  const key = await window.crypto.subtle.importKey(
    'raw',
    passwordBuffer,
    { name: 'PBKDF2' },
    false,
    ['deriveBits', 'deriveKey'],
  );
  return window.crypto.subtle.deriveKey(
    ops,
    key,
    { name: ALGORITHM, length: 256 },
    true,
    ['encrypt', 'decrypt'],
  );
};

// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
export async function encrypt(data: string, password: string): Promise<string> {
  const enc = new TextEncoder();
  const dataBuffer = enc.encode(data);
  const key = await generateKey(password);
  const iv = window.crypto.getRandomValues(new Uint8Array(12));
  const encryptedContent = await window.crypto.subtle.encrypt(
    { name: ALGORITHM, iv: iv },
    key,
    dataBuffer,
  );
  const buffer = new Uint8Array(iv.length + encryptedContent.byteLength);
  buffer.set(iv, 0);
  buffer.set(new Uint8Array(encryptedContent), iv.length);
  return ab2base64(buffer.buffer);
}

// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
export async function decrypt(data: string, password: string): Promise<string> {
  const dataBuffer = base642ab(data);
  const iv = new Uint8Array(dataBuffer, 0, 12);
  const encryptedData = new Uint8Array(dataBuffer, 12);
  const key = await generateKey(password);
  const decryptedContent = await window.crypto.subtle.decrypt(
    { name: ALGORITHM, iv: iv },
    key,
    encryptedData,
  );
  const dec = new TextDecoder();
  return dec.decode(decryptedContent);
}
0

There are 0 best solutions below