Skip to content

Latest commit

 

History

History
257 lines (191 loc) · 9.84 KB

README.md

File metadata and controls

257 lines (191 loc) · 9.84 KB

Send encrypted files to any Ethereum address

This demo application allows you to send encrypted files to any Ethereum address. It uses Identity-based encryption (IBE) together with the vetKeys (Verifiably Encrypted Threshold Key Derivation) Internet Computer feature.

TL;DR vetKeys on the Internet Computer allow developers to more easily perform encryption, threshold decryption, and signing. vetKeys allows a public blockchain to hold secrets.

Contributors Forks Stargazers Issues MIT License

Note

The vetKeys feature will be available Q2 2025. This demo uses the chainkey_testing_canister to simulate vetKeys.

Screenshot

Table of Contents

Sending a file

The user signs in using their Ethereum address with the help of the ic-siwe provider canister and its support library, ic-siwe-js.

The sign in process securely establishes a link between the user's Ethereum address and their Internet Identity.

Create a transfer

The user can create a transfer by specifying the recipient's Ethereum address and the file to send.

When Send file is clicked, this is what happens:

Get the recipient's public key

The frontend calls the vetkd_public_key method on the chainkey_testing_canister to get the recipient's public key. The recipient's Ethereum address is used as the identity for the IBE scheme that generates the public key.

📄 vetkd_public_key.rs

#[update]
async fn vetkd_public_key(address: String) -> Result<Vec<u8>, String> {
    let address = Address::parse_checksummed(address, None).map_err(|e| e.to_string())?;

    let args = VetkdPublicKeyArgs {
        key_id: VetkdPublicKeyArgsKeyId {
            name: "insecure_test_key_1".to_string(),
            curve: VetkdCurve::Bls12381,
        },
        derivation_path: vec![ByteBuf::from(*address.0)],
        canister_id: None,
    };

    let (result,) = chainkey_testing_canister
        .vetkd_public_key(args)
        .await
        .unwrap();

    Ok(result.public_key.to_vec())
}

After the vetKeys feature is available, the testing canister call would be replaced with the actual vetKeys call.

Encrypt the file in the frontend

The frontend encrypts the file using the recipient's public key. The file is encrypted using the vetkd.IBECiphertext.encrypt method from the ic-vetkd-utils support package. This package will be available in a new version when the vetKeys feature is available.

📄 useTransferCreate.tsx

/// ... 
const response = await backend.vetkd_public_key(recipientAddress);
if ("Err" in response) {
  console.error("Error getting recipient public key", response.Err);
  return;
}

const recipientPublicKey = response.Ok as Uint8Array;
const seed = window.crypto.getRandomValues(new Uint8Array(32));
const fileBuffer = await file.arrayBuffer();
const encodedMessage = new Uint8Array(fileBuffer);
const encryptedFile = vetkd.IBECiphertext.encrypt(
  recipientPublicKey,
  new Uint8Array(0),
  encodedMessage,
  seed
);
const request = {
  to: recipientAddress,
  content_type: file.type,
  filename: file.name,
  data: encryptedFile.serialize(),
};
return backend.transfer_create(request);

Store the encrypted file in the backend

The backend canister verifies the from and to parameters and stores the encrypted file.

📄 transfer_create.rs

#[update]
pub async fn transfer_create(args: TransferCreateRequest) -> Result<Transfer, String> {
    let principal_blob = principal_to_blob(ic_cdk::caller());
    let from = UserManager::get(principal_blob).ok_or("User not found".to_string())?;
    let to = Address::parse_checksummed(args.to, None).map_err(|e| e.to_string())?;
    let transfer = TransferManager::create(TransferManagerCreateArgs {
        from,
        to,
        filename: args.filename,
        content_type: args.content_type,
        data: args.data,
    })?;
    Ok(transfer)
}

Receiving a file

A receiveing user logs in with their Ethereum address same as the sender. The frontend calls the transfer_list method on the backend to get a list of transfers. The user can then download the encrypted file and decrypt it using their private key.

📄 useTransferList.tsx

vetKeys on the Internet Computer allow developers to more easily perform encryption, threshold decryption, and signing when building dapps on ICP. It is powered by a protocol called vetKD (Verifiably Encrypted Threshold Key Derivation) that allows to derive decryption keys on demand.

Getting the private key

One of the challenges when doing encryption in an open network like the Internet Computer is how to securely store and retrieve the private key. vetKeys solves this problem by allowing the user to derive the private key on demand. To securely transfer the private key to the requesting user, a transport secret key is used to encrypt the private key while in transfer.

📄 useVetkdEncryptedKey.tsx

// ...
const seed = window.crypto.getRandomValues(new Uint8Array(32));
const transportSecretKey = new vetkd.TransportSecretKey(seed);
const response = await backend?.vetkd_encrypted_key(
  transportSecretKey.public_key(),
);

Similar to when getting the public key, the backend uses the Ethereum address of the caller as the key derivation path. Also note how the transport secret key is passed to vetkd_encrypted_key function to encrypt the private key before returning it to the frontend.

📄 vetkd_encrypted_key.rs

#[update]
async fn vetkd_encrypted_key(encryption_public_key: Vec<u8>) -> Result<Vec<u8>, String> {
    let address = get_caller_address().await?;

    let args = VetkdEncryptedKeyArgs {
        key_id: VetkdEncryptedKeyArgsKeyId {
            name: "insecure_test_key_1".to_string(),
            curve: VetkdCurve::Bls12381,
        },
        public_key_derivation_path: vec![ByteBuf::from(*address.0)],
        derivation_id: ByteBuf::new(),
        encryption_public_key: ByteBuf::from(encryption_public_key),
    };

    let (result,) = chainkey_testing_canister
        .vetkd_encrypted_key(args)
        .await
        .unwrap();

    Ok(result.encrypted_key.to_vec())
}

Decrypting the file

Once in posession of the encrypted private key, the frontend can first decrypt the private key using the transport secret key and then use the private key to decrypt the file.

📄 useTransferGet.tsx

// ...
const key = transportSecretKey.decrypt(
  encryptedKey,
  publicKey!,
  new Uint8Array()
);
const ibeCiphertext = vetkd.IBECiphertext.deserialize(
  transfer.data as Uint8Array
);
const decryptedData = ibeCiphertext.decrypt(key);
return { decryptedData, ...transfer };

Run locally

Pre-requisites:

Once you have the prerequisites installed, you can clone this repository and run the project.

dfx start --clean --background
pnpm i
bash scripts/deploy.sh

Run frontend in development mode

pnpm run dev

Deploy to the Internet Computer

bash scripts/deploy-ic.sh

License

This project is licensed under the MIT License. See the LICENSE file for more details.

Contributing

Contributions are welcome! Please open an issue or submit a pull request if you have any suggestions or improvements.