Encrypt.ink is a free opensource web app that provides secure file encryption in the browser with advanced wallet integration for seamless cryptographic operations.
The libsodium library is used for all cryptographic algorithms. Technical details here.
It's easy to self host and deploy encrypt.ink, you can do that either with npm or docker
If you wish to self host the app please follow these instructions:
Before installation make sure you are running nodejs and have npm installed
git clone https://github.com/encrypt-ink/encrypt.ink.git encrypt.ink
cd encrypt.ink
npm install
npm run build
npm run start
the app should be running on port 3391.
if you wish to run the app in development enviroment run :
npm run dev
You can install the app with docker in multiple ways. You are free to choose which method you like.
docker pull encryptink/encrypt.ink:latest
docker run -d -p 3991:80 encryptink/encrypt.ink
git clone https://github.com/encrypt-ink/encrypt.ink.git encrypt.ink
cd encrypt.ink
docker build . -t encryptink/encrypt.ink
docker run -d -p 3991:80 encryptink/encrypt.ink
git clone https://github.com/encrypt-ink/encrypt.ink.git encrypt.ink
cd encrypt.ink
docker compose build
docker compose up
The app should be running on port 3991.
encrypt.ink is also available as a Docker image. You can find it on Docker Hub.
Never share your private key to anyone! Only public keys should be exchanged.
Encrypt.ink revolutionizes file encryption by integrating Web3 wallets as cryptographic key sources. Instead of relying solely on traditional passwords, users can leverage their existing blockchain wallets to encrypt and decrypt files securely. This approach combines the security of blockchain cryptography with the convenience of wallet-based authentication.
When using wallet-based encryption, Encrypt.ink employs a sophisticated key derivation mechanism:
For enterprise or shared file scenarios:
// Wallet signature-based key derivation
const message = "Encrypt.ink file encryption";
const signature = await wallet.signMessage(message);
const seed = sha256(signature);
const encryptionKey = hkdf(seed, 32, "file-encryption");
Files encrypted with encrypt.ink are identifiable by looking at the file signature that is used by the app to verify the content of a file, Such signatures are also known as magic numbers or Magic Bytes. These Bytes are authenticated and cannot be changed.
Safari and Mobile browsers are limited to a single file with maximum size of 1GB due to some issues related to service-workers. In addition, this limitation also applies when the app fails to register the service-worker (e.g FireFox Private Browsing).
If you plan on sending someone an encrypted file, it is recommended to use your private key and their public key to encrypt the file.
The file can be shared in any safe file sharing app.
Public keys are allowed to be shared, they can be sent as .public
file or as text.
Never share your private key to anyone! Only public keys should be exchanged.
Make sure to store your encryption keys in a safe place and make a backup to an external storage.
Storing your private key in cloud storage is not recommended!
No, encrypt.ink never stores any of your data. It only runs locally in your browser.
Yes, encrypt.ink is free and always will be. However, please consider donating to support the project.
Encrypt.ink accepts all file types. There's no file size limit, meaning files of any size can be encrypted.
Safari browser and mobile/smartphones browsers are limited to 1GB.
It means that your browser doesn't support the server-worker fetch api. Hence, you are limited to small size files. see Limitations for more info.
Yes. Public keys are allowed to be shared, they can be sent as .public
file or as text.
But make sure to never share your private key with anyone!
Encrypt.ink uses authenticated encryption. The sender must provide their private key, a new shared key will be computed from both keys to encrypt the file. Recipient has to provide their private key when decrypting also. this way can verify that the encrypted file was not tampered with, and was sent from the real sender.
Nope. lost private keys cannot be recovered.
Also, if you feel that your private key has been compromised (e.g accidentally shared / computer hacked) then you must decrypt all files that were encrypted with that key, generate a new keypair and re-encrypt the files.
Once you visit the site and the page loads, it runs only offline. Wallet connections are handled locally in your browser.
Yes! Hardware wallets like Ledger provide the highest security for wallet-based encryption. The signing happens on the hardware device, ensuring your private keys never leave the secure element.
If you lose access to your wallet and don't have the seed phrase, files encrypted with that wallet cannot be decrypted. Always backup your wallet seed phrase securely. Consider using multiple encryption methods for critical files.
No, files can only be decrypted with the same wallet that encrypted them. Each wallet generates a unique encryption key based on its private key signature. This ensures cryptographic security and access control.
We support Solana wallets including Phantom, Solflare, Glow, Torus, and Ledger. Both software and hardware wallets are supported.
Encrypt.ink is an open-source application. You can help make it better by making commits on GitHub. The project is maintained in my free time. Donations of any size are appreciated.
Password hashing functions derive a secret key of any size from a password and a salt.
let salt = sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES);
let key = sodium.crypto_pwhash(
sodium.crypto_secretstream_xchacha20poly1305_KEYBYTES,
password,
salt,
sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
sodium.crypto_pwhash_ALG_ARGON2ID13
);
The crypto_pwhash()
function derives an 256 bits long key from a password and a salt salt whose fixed length is 128 bits, which should be unpredictable.
randombytes_buf()
is the easiest way to fill the 128 bits of the salt.
OPSLIMIT
represents a maximum amount of computations to perform.
MEMLIMIT
is the maximum amount of RAM that the function will use, in bytes.
crypto_pwhash_OPSLIMIT_INTERACTIVE
and crypto_pwhash_MEMLIMIT_INTERACTIVE
provide base line for these two parameters. This currently requires 64 MiB of dedicated RAM. which is suitable for in-browser operations.
crypto_pwhash_ALG_ARGON2ID13
using the Argon2id algorithm version 1.3.
In order to use the app to encrypt a file, the user has to provide a valid file and a password. this password gets hashed and a secure key is derived from it with Argon2id to encrypt the file.
let res = sodium.crypto_secretstream_xchacha20poly1305_init_push(key);
header = res.header;
state = res.state;
let tag = last
? sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL
: sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
let encryptedChunk = sodium.crypto_secretstream_xchacha20poly1305_push(
state,
new Uint8Array(chunk),
null,
tag
);
stream.enqueue(signature, salt, header, encryptedChunk);
The crypto_secretstream_xchacha20poly1305_init_push
function creates an encrypted stream where it initializes a state
using the key and an internal, automatically generated initialization vector. It then stores the stream header into header
that has a size of 192 bits.
This is the first function to call in order to create an encrypted stream. The key will not be required any more for subsequent operations.
An encrypted stream starts with a short header, whose size is 192 bits. That header must be sent/stored before the sequence of encrypted messages, as it is required to decrypt the stream. The header content doesn't have to be secret because decryption with a different header would fail.
A tag is attached to each message accoring to the value of last
, which indicates if that is the last chunk of the file or not. That tag can be any of:
crypto_secretstream_xchacha20poly1305_TAG_MESSAGE
: This doesn't add any information about the nature of the message.crypto_secretstream_xchacha20poly1305_TAG_FINAL
: This indicates that the message marks the end of the stream, and erases the secret key used to encrypt the previous sequence.The crypto_secretstream_xchacha20poly1305_push()
function encrypts the file chunk
using the state
and the tag
, without any additional information (null
).
the XChaCha20 stream cipher Poly1305 MAC authentication are used for encryption.
stream.enqueue()
function adds the encrypt.ink signature(magic bytes), salt and header followed by the encrypted chunks.
let state = sodium.crypto_secretstream_xchacha20poly1305_init_pull(header, key);
let result = sodium.crypto_secretstream_xchacha20poly1305_pull(
state,
new Uint8Array(chunk)
);
if (result) {
let decryptedChunk = result.message;
stream.enqueue(decryptedChunk);
if (!last) {
// continue decryption
}
}
The crypto_secretstream_xchacha20poly1305_init_pull()
function initializes a state given a secret key
and a header
. The key is derived from the password provided during the decryption, and the header sliced from the file. The key will not be required any more for subsequent operations.
The crypto_secretstream_xchacha20poly1305_pull()
function verifies that the chunk
contains a valid ciphertext and authentication tag for the given state
.
This function will stay in a loop, until a message with the crypto_secretstream_xchacha20poly1305_TAG_FINAL
tag is found.
If the decryption key is incorrect the function returns an error.
If the ciphertext or the authentication tag appear to be invalid it returns an error.
let password = sodium.to_base64(
sodium.randombytes_buf(16),
sodium.base64_variants.URLSAFE_NO_PADDING
);
return password;
The randombytes_buf()
function fills 128 bits starting at buf with an unpredictable sequence of bytes.
The to_base64()
function encodes buf as a Base64 string without padding.
const keyPair = sodium.crypto_kx_keypair();
let keys = {
publicKey: sodium.to_base64(keyPair.publicKey),
privateKey: sodium.to_base64(keyPair.privateKey),
};
return keys;
The crypto_kx_keypair()
function randomly generates a secret key and a corresponding public key. The public key is put into publicKey and the secret key into privateKey. both of 256 bits.
let key = sodium.crypto_kx_client_session_keys(
sodium.crypto_scalarmult_base(privateKey),
privateKey,
publicKey
);
Using the key exchange API, two parties can securely compute a set of shared keys using their peer's public key and their own secret key.
The crypto_kx_client_session_keys()
function computes a pair of 256 bits long shared keys using the recipient's public key, the sender's private key.
The crypto_scalarmult_base()
function used to compute the sender's public key from their private key.
For wallet-based encryption, Encrypt.ink implements a deterministic key derivation process:
// Wallet signature for key derivation
const message = new TextEncoder().encode("Encrypt.ink file encryption");
const signature = await wallet.signMessage(message);
// Derive encryption key from signature
const keyMaterial = await crypto.subtle.importKey(
"raw",
signature,
"HKDF",
false,
["deriveKey"]
);
const encryptionKey = await crypto.subtle.deriveKey(
{
name: "HKDF",
salt: new Uint8Array(32), // Static salt for deterministic results
info: new TextEncoder().encode("file-encryption"),
hash: "SHA-256",
},
keyMaterial,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);
The wallet signing process ensures:
// Solana wallet provider detection and connection
const detectWallet = () => {
if (window.solana?.isPhantom) return 'phantom';
if (window.solana?.isGlow) return 'glow';
if (window.solana?.isSolflare) return 'solflare';
return null;
};
// Secure message signing with validation
const signForEncryption = async (wallet, message) => {
try {
const signature = await wallet.signMessage(message);
// Verify signature before use
const isValid = await verifySignature(message, signature, wallet.publicKey);
if (!isValid) throw new Error('Invalid signature');
return signature;
} catch (error) {
throw new Error(`Wallet signing failed: ${error.message}`);
}
};
XChaCha20 is a variant of ChaCha20 with an extended nonce, allowing random nonces to be safe.
XChaCha20 doesn't require any lookup tables and avoids the possibility of timing attacks.
Internally, XChaCha20 works like a block cipher used in counter mode. It uses the HChaCha20 hash function to derive a subkey and a subnonce from the original key and extended nonce, and a dedicated 64-bit block counter to avoid incrementing the nonce after each block.