Here's my React web app for encrypt / decrypt text messages and files using user-chosen password:

File: src/components/EncryptDecryptForm.js
```js
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { encryptMessage, decryptMessage } from '../CryptoService';
import Linkify from 'linkify-react';
import ClipboardJS from 'clipboard';
import { isSafari, isMobileSafari } from 'react-device-detect';

function EncryptDecryptForm() {
  const { t } = useTranslation();
  const [password, setPassword] = useState(localStorage.getItem('password') || '');
  const [message, setMessage] = useState('');
  const [encryptedMessage, setEncryptedMessage] = useState('');
  const [decryptedMessage, setDecryptedMessage] = useState('');
  const [file, setFile] = useState(null);
  const [error, setError] = useState(null);

  const toBase64 = (message) => window.btoa(String.fromCharCode(...new Uint8Array(message)));
  const fromBase64 = (message) => window.atob(message);

  const handleMessageEncrypt = async () => {
    const encryptMessageAndBase64 = async () => {
      const result = toBase64(await encryptMessage(message, password))
      setEncryptedMessage(result);
      return result;
    };

    if(isSafari || isMobileSafari) {
      // Hack for Safari because on Safari `navigator.clipboard.write` has to be the FIRST asynchronous function to be called: https://stackoverflow.com/questions/66312944/javascript-clipboard-api-write-does-not-work-in-safari
      // And
      const makeBase64EncryptedMessagePromise = async () => {
        const base64Encrypted = await encryptMessageAndBase64();
        return new Blob([base64Encrypted], { type: 'text/plain' });
      };
      navigator.clipboard.write([new ClipboardItem({ 'text/plain': makeBase64EncryptedMessagePromise() })])
        .then(function () { setError(null); })
        .catch(function (err) { setError(`${t('encryptionError')}: ${err.message}`); });
    } else {
      // We cannot use navigator.clipboard due to:
      // calling navigator.clipboard API within Android Webview (e.g. within Wechat app) yields "Write permission denied": https://bugs.chromium.org/p/chromium/issues/detail?id=1271620 and https://zhuanlan.zhihu.com/p/597944027
      try {
        const base64Encrypted = await encryptMessageAndBase64();
        ClipboardJS.copy(base64Encrypted);
        setError(null);
      } catch (err) {
        setError(`${t('encryptionError')}: ${err.message}`);
      }
    }
  };

  const handleMessageDecrypt = async () => {
    try {
      const decrypted = await decryptMessage(fromBase64(message), password);
      setDecryptedMessage(new TextDecoder().decode(decrypted));
      setError(null);
    } catch (err) {
      handleDecryptionError(err);
    }
  };

  const handleFileChange = (event) => {
    setFile(event.target.files[0]);
  };

  const handleFileEncrypt = async () => {
    const reader = new FileReader();
    reader.onload = async (event) => {
      const encryptedFileContent = await encryptMessage(new Uint8Array(event.target.result), password);
      const blob = new Blob([encryptedFileContent], { type: file.type });
      const url = URL.createObjectURL(blob);
      const link = document.createElement('a');
      link.href = url;
      link.download = `encrypted_${file.name}`;
      link.click();
      setError(null);
    };
    reader.readAsArrayBuffer(file);
  };

  const handleFileDecrypt = async () => {
    const reader = new FileReader();
    reader.onload = async (event) => {
      try {
        const decrypted = await decryptMessage(new Uint8Array(event.target.result), password);
        const blob = new Blob([decrypted], { type: file.type });
        const url = URL.createObjectURL(blob);
        const link = document.createElement('a');
        link.href = url;
        link.download = `decrypted_${file.name}`;
        link.click();
        setError(null);
      } catch (err) {
        handleDecryptionError(err);
      }
    };
    reader.readAsArrayBuffer(file);
  };

  const handleDecryptionError = (err) => {
    if (err.name === 'OperationError') {
      setError(t('invalidPasswordError'));
    } else if (err.name === 'InvalidCharacterError') {
      setError(t('corruptedMessageError'));
    } else {
      setError(`${t('unknownError')}: ${err.message}`);
    }
  };

  const linkifyOptions = {
    rel: "noopener noreferrer",
    target: "_blank",
  };

  return (
    <div>
      <input id="password" type="password" autoComplete="current-password" value={password} onChange={e => {
        setPassword(e.target.value);
        localStorage.setItem('password', e.target.value);
      }} placeholder={t('passwordPlaceholder')} />
      <input id="message" type="text" value={message} onChange={e => setMessage(e.target.value)} placeholder={t('messagePlaceholder')} />
      <button onClick={handleMessageEncrypt}>{t('encryptMessageAndCopyToClipboard')}</button>
      <p>{t('encryptedMessage')}:</p>
      <pre>{encryptedMessage}</pre>
      <button onClick={handleMessageDecrypt}>{t('decryptMessage')}</button>
      <p>{t('decryptedMessage')}:</p>
      <Linkify as="pre" options={linkifyOptions}>{decryptedMessage}</Linkify>
      <p>{error && <span className="error">{error}</span>}</p>
      <hr />
      <input id="file" type="file" onChange={handleFileChange} />
      <button onClick={handleFileEncrypt}>{t('encryptSelectedFile')}</button>
      <button onClick={handleFileDecrypt}>{t('decryptSelectedFile')}</button>
    </div>
  );

}

export default EncryptDecryptForm;
```

File: src/CryptoService.js
```js
async function makeKeys(password) {
  const keyData = new TextEncoder().encode(password);
  const rawKey = await window.crypto.subtle.importKey(
    'raw',
    keyData,
    { name: 'PBKDF2' },
    false,
    ['deriveKey']
  );
  const derivedKey = await window.crypto.subtle.deriveKey(
    { name: 'PBKDF2', salt: keyData, iterations: 1000, hash: 'SHA-256' },
    rawKey,
    { name: 'AES-GCM', length: 256 },
    false,
    ['encrypt', 'decrypt']
  );
  const algorithm = { name: 'AES-GCM', iv: keyData };
  return { algorithm, key: derivedKey };
}

export async function encryptMessage(message, password) {
  const data = typeof message === 'string' ? new TextEncoder().encode(message) : message;
  const { algorithm, key } = await makeKeys(password);
  return await window.crypto.subtle.encrypt(algorithm, key, data);
}

export async function decryptMessage(message, password) {
  const data = typeof message === 'string' ? Uint8Array.from(message, c => c.charCodeAt(0)) : message;
  const { algorithm, key } = await makeKeys(password);
  return await window.crypto.subtle.decrypt(algorithm, key, data);
}

export async function encryptFile(originalFileName, fileContents, password, newFileName = '') {
  // TODO:
  // 1. to generate `wrappedFileContents`:
  //   1.1 the first 4 bytes of `wrappedFileContents` is unsigned int32 which is set to the length of `originalFileName`. It should throw error if the `originalFileName` is empty / null or longer than maximum  that uint32 can hold.
  //   1.2 The above 4 bytes is followed by `originalFileName`, and then `fileContents`
  // 2. to generate `encryptedFileContents`: call `encryptMessage()` on `wrappedFileContents`
  // 3. to generate `newFileName`: if the `newFileName` parameter is empty string, it should be generated and have following format: "RANDOMWORD_RANDOMNUMBER" where RANDOMNUMBER is random 4 digit number (for example "alumni_8651" is a valid newFileName). Consider using state-of-the-art open source library for RANDOMWORD. If the `newFileName` parameter is not empty then just use it instead of generation.
  return { encryptedFileContents, newFileName }
}

export async function decryptFile(encryptedFileContents, password) {
  // TODO:
  // 1. call `decryptMessage()` on `encryptedFileContents` in order to get `cryptexFileContents`
  // 2. Parse the first 4 bytes of `cryptexFileContents` to get `fileNameLength`. Throw error if `cryptexFileContents` is less than 4 bytes, or `fileNameLength` is not valid unsigned int32 number.
  // 3. Read from the 5th bytes until `fileNameLength` to get `originalFileName`. Throw error if not enough bytes to read (put it another way, the length of `cryptexFileContents` MUST BE greater than 4 bytes + `fileNameLength`)
  // 4. Read the rest of bytes to get `decryptedFileContents`.
  return { originalFileName, decryptedFileContents}
}
```


I can describe the current user journey for file encryption as follows:
1. User input the password and select file "example.zip", and then hit encryptSelectedFile button. 
2. The app will encrypt the file using the password, and auto-trigger download of "encrypted_example.zip"

The current user journey for file decryption is as follows:
1. User input the correct password and select file "encrypted_example.zip", and hit decryptSelectedFile button.
2. The app will decrypt the file using the password, and auto-trigger download of "decrypted_encrypted_example.zip"

The problem with current user journey is information can leak from file name because it's not encrypted. I'd like to modify the web app so that during file encryption, file name is stored within the binary contents and encrypted altogether. And during file decryption phase, the app will decrypt to get original file name and original file contents, and restore the original file name.
I have written TODO instructions in CryptoService.js to show you how it should be done.
Can you write the acutal code for me using state-of-the-art tech stack and best practices?