mirror of
https://github.com/Sevi-py/tnyr.me.git
synced 2026-04-03 03:00:21 -04:00
implement client sided encryption and decryption
This commit is contained in:
11
README.md
11
README.md
@@ -18,15 +18,16 @@ A secure, self-hosted URL shortener with end-to-end encryption. Perfect for priv
|
||||
## end-to-end Encryption Process
|
||||
|
||||
1. **ID Generation**
|
||||
- Unique random ID created for each link (e.g. `R53nSAg`)
|
||||
- Example: `google.com` → `tnyr.me/R53nSAg`
|
||||
- Unique random ID created for each link (e.g. `iA4y6jMjFk`)
|
||||
- Example: `google.com` → `tnyr.me/#iA4y6jMjFk`
|
||||
|
||||
2. **Hashing**
|
||||
- Two Argon2 hashes are calculated by using different salts
|
||||
- Two Scrypt hashes are calculated by using different salts
|
||||
- Original URL encrypted with AES-256 using Hash 2
|
||||
- The whole encryption and decryption process happens in the browser
|
||||
|
||||
3. **Storage**
|
||||
- Original URL encrypted with AES-256-GCM using Hash 2
|
||||
- Only Hash 1 (storage key) is saved in database
|
||||
- Only Hash 1 (storage key) and the encrypted URL are saved in database
|
||||
|
||||
## Development Setup
|
||||
|
||||
|
||||
1
backend/dist/assets/index-C46YJTI0.css
vendored
Normal file
1
backend/dist/assets/index-C46YJTI0.css
vendored
Normal file
File diff suppressed because one or more lines are too long
110
backend/dist/assets/index-CQa4OsAb.js
vendored
Normal file
110
backend/dist/assets/index-CQa4OsAb.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
backend/dist/assets/index-DAWtUNlD.css
vendored
1
backend/dist/assets/index-DAWtUNlD.css
vendored
File diff suppressed because one or more lines are too long
105
backend/dist/assets/index-Dqm_4jPr.js
vendored
105
backend/dist/assets/index-Dqm_4jPr.js
vendored
File diff suppressed because one or more lines are too long
78
backend/dist/index.html
vendored
78
backend/dist/index.html
vendored
@@ -1,39 +1,39 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/favicon/favicon-96x96.png" sizes="96x96" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/favicon/favicon.ico" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png" />
|
||||
<meta name="apple-mobile-web-app-title" content="tnyr.me" />
|
||||
<link rel="manifest" href="/favicon/site.webmanifest" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="An easy to use end-to-end encrypted URL shortener. tnyr.me is privacy friendly and does not track you or store cookies" />
|
||||
<link rel="canonical" href="https://tnyr.me" />
|
||||
|
||||
<meta property="og:title" content="TNYR - Privacy friendly URL shortener" />
|
||||
<meta property="og:site_name" content="tynr.me">
|
||||
<meta property="og:url" content="https://tnyr.me">
|
||||
<meta property="og:description" content="A self encrypted url shortener that puts your privacy first, while being easy to use. tnyr.me doesn't track users, store cookies or log your requests.">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:image" content="https://tnyr.me/meta/logo.png">
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
"name": "TNYR",
|
||||
"url": "https://tnyr.me",
|
||||
"description": "A self encrypted url shortener that puts your privacy first, while being easy to use. tnyr.me doesn't track users, store cookies or log your requests."
|
||||
}
|
||||
</script>
|
||||
|
||||
<title>tnyr.me - Privacy friendly URL shortener</title>
|
||||
<script type="module" crossorigin src="/assets/index-Dqm_4jPr.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DAWtUNlD.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
</body>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/favicon/favicon-96x96.png" sizes="96x96" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/favicon/favicon.ico" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png" />
|
||||
<meta name="apple-mobile-web-app-title" content="tnyr.me" />
|
||||
<link rel="manifest" href="/favicon/site.webmanifest" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="An easy to use end-to-end encrypted URL shortener. tnyr.me is privacy friendly and does not track you or store cookies" />
|
||||
<link rel="canonical" href="https://tnyr.me" />
|
||||
|
||||
<meta property="og:title" content="TNYR - Privacy friendly URL shortener" />
|
||||
<meta property="og:site_name" content="tynr.me">
|
||||
<meta property="og:url" content="https://tnyr.me">
|
||||
<meta property="og:description" content="A self encrypted url shortener that puts your privacy first, while being easy to use. tnyr.me doesn't track users, store cookies or log your requests.">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:image" content="https://tnyr.me/meta/logo.png">
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
"name": "TNYR",
|
||||
"url": "https://tnyr.me",
|
||||
"description": "A self encrypted url shortener that puts your privacy first, while being easy to use. tnyr.me doesn't track users, store cookies or log your requests."
|
||||
}
|
||||
</script>
|
||||
|
||||
<title>tnyr.me - Privacy friendly URL shortener</title>
|
||||
<script type="module" crossorigin src="/assets/index-CQa4OsAb.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-C46YJTI0.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -100,8 +100,8 @@ def decrypt_url(key, iv, ciphertext):
|
||||
|
||||
return plaintext.decode()
|
||||
|
||||
@app.route('/shorten', methods=['POST'])
|
||||
def shorten_url():
|
||||
@app.route('/shorten-server', methods=['POST'])
|
||||
def shorten_url_server():
|
||||
data = request.get_json()
|
||||
|
||||
if not data or 'url' not in data:
|
||||
@@ -147,7 +147,64 @@ def shorten_url():
|
||||
|
||||
return jsonify({"id": id}), 201
|
||||
|
||||
@app.route('/<id>')
|
||||
@app.route('/shorten', methods=['POST'])
|
||||
def shorten_url_client():
|
||||
data = request.get_json()
|
||||
required_fields = ['LOOKUP_HASH', 'ENCRYTION_SALT', 'IV', 'ENCRYPTED_URL']
|
||||
|
||||
if not data or not all(field in data for field in required_fields):
|
||||
missing_fields = [field for field in required_fields if field not in (data or {})]
|
||||
return jsonify({"error": f"Missing fields: {', '.join(missing_fields)}"}), 400
|
||||
|
||||
lookup_hash = data['LOOKUP_HASH']
|
||||
|
||||
try:
|
||||
encryption_salt = bytes.fromhex(data['ENCRYTION_SALT'])
|
||||
iv = bytes.fromhex(data['IV'])
|
||||
encrypted_url = bytes.fromhex(data['ENCRYPTED_URL'])
|
||||
except (ValueError, TypeError):
|
||||
return jsonify({"error": "Invalid hex format for salt, IV, or encrypted URL"}), 400
|
||||
|
||||
with get_db() as conn:
|
||||
cur = conn.execute(
|
||||
"SELECT 1 FROM client_side_urls WHERE lookup_hash = ?",
|
||||
(lookup_hash,)
|
||||
)
|
||||
if cur.fetchone():
|
||||
return jsonify({"error": "Lookup hash already exists"}), 409
|
||||
|
||||
conn.execute(
|
||||
"INSERT INTO client_side_urls (lookup_hash, encryption_salt, iv, encrypted_url) VALUES (?, ?, ?, ?)",
|
||||
(lookup_hash, encryption_salt, iv, encrypted_url)
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
return jsonify({"message": "URL shortened successfully"}), 201
|
||||
|
||||
@app.route('/get-encrypted-url', methods=['GET'])
|
||||
def get_encrypted_url():
|
||||
lookup_hash = request.args.get('lookup_hash')
|
||||
|
||||
if not lookup_hash:
|
||||
return jsonify({"error": "Missing lookup_hash parameter"}), 400
|
||||
|
||||
with get_db() as conn:
|
||||
cur = conn.execute(
|
||||
"SELECT encryption_salt, iv, encrypted_url FROM client_side_urls WHERE lookup_hash = ?",
|
||||
(lookup_hash,)
|
||||
)
|
||||
row = cur.fetchone()
|
||||
|
||||
if not row:
|
||||
return jsonify({"error": "Link not found"}), 404
|
||||
|
||||
return jsonify({
|
||||
"ENCRYTION_SALT": row['encryption_salt'].hex(),
|
||||
"IV": row['iv'].hex(),
|
||||
"ENCRYPTED_URL": row['encrypted_url'].hex()
|
||||
}), 200
|
||||
|
||||
@app.route('/<id>') # Still needed for old links
|
||||
def redirect_url(id):
|
||||
id_bytes = id.encode()
|
||||
|
||||
|
||||
@@ -2,4 +2,11 @@ CREATE TABLE IF NOT EXISTS urls (
|
||||
lookup_hash TEXT PRIMARY KEY,
|
||||
iv BLOB NOT NULL,
|
||||
encrypted_url BLOB NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS client_side_urls (
|
||||
lookup_hash TEXT PRIMARY KEY,
|
||||
encryption_salt BLOB NOT NULL,
|
||||
iv BLOB NOT NULL,
|
||||
encrypted_url BLOB NOT NULL
|
||||
);
|
||||
13
frontend/package-lock.json
generated
13
frontend/package-lock.json
generated
@@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
"@icons-pack/react-simple-icons": "^11.2.0",
|
||||
"@noble/hashes": "^1.8.0",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.5",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
@@ -1033,6 +1034,18 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
||||
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
"@icons-pack/react-simple-icons": "^11.2.0",
|
||||
"@noble/hashes": "^1.8.0",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.5",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { scrypt } from "@noble/hashes/scrypt";
|
||||
import { Input } from "./components/ui/input";
|
||||
import { Button } from "./components/ui/button";
|
||||
import { Shield, Key, Hash, Lock, Copy, EyeOff, Github } from "lucide-react";
|
||||
import { Shield, Key, Hash, Lock, Copy, EyeOff, Github, Loader2 } from "lucide-react";
|
||||
import { SiBuymeacoffee } from "@icons-pack/react-simple-icons";
|
||||
|
||||
const urlSchema = z.object({
|
||||
@@ -27,9 +28,143 @@ const urlSchema = z.object({
|
||||
|
||||
type UrlFormData = z.infer<typeof urlSchema>;
|
||||
|
||||
// Configuration constants
|
||||
const ALLOWED_CHARS = 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ123456789+*-';
|
||||
// Shared salt for lookup hash (protects against rainbow tables)
|
||||
const LOOKUP_SALT = new Uint8Array([0x74, 0x6e, 0x79, 0x72, 0x2e, 0x6d, 0x65, 0x5f, 0x6c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x5f, 0x73]); // "tnyr.me_lookup_s"
|
||||
|
||||
// Crypto utilities
|
||||
const generateRandomString = (length: number, chars: string = ALLOWED_CHARS) => {
|
||||
const array = new Uint8Array(length);
|
||||
crypto.getRandomValues(array);
|
||||
return Array.from(array, byte => chars[byte % chars.length]).join('');
|
||||
};
|
||||
|
||||
const generateRandomBytes = (length: number) => {
|
||||
const array = new Uint8Array(length);
|
||||
crypto.getRandomValues(array);
|
||||
return array;
|
||||
};
|
||||
|
||||
// Hash ID for lookup (using shared salt to protect against rainbow tables)
|
||||
const hashIdForLookup = (id: string) => {
|
||||
const encoder = new TextEncoder();
|
||||
return scrypt(encoder.encode(id), LOOKUP_SALT, {
|
||||
N: 2 ** 17, // CPU/memory cost
|
||||
r: 8, // block size
|
||||
p: 1, // parallelism
|
||||
dkLen: 32 // output length
|
||||
});
|
||||
};
|
||||
|
||||
// Hash ID with random salt for encryption key
|
||||
const deriveEncryptionKey = (id: string, salt: Uint8Array) => {
|
||||
const encoder = new TextEncoder();
|
||||
return scrypt(encoder.encode(id), salt, {
|
||||
N: 2 ** 17, // CPU/memory cost
|
||||
r: 8, // block size
|
||||
p: 1, // parallelism
|
||||
dkLen: 32 // output length
|
||||
});
|
||||
};
|
||||
|
||||
const encryptUrl = async (key: Uint8Array, plaintext: string) => {
|
||||
const iv = generateRandomBytes(16);
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(plaintext);
|
||||
|
||||
const cryptoKey = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
key,
|
||||
{ name: 'AES-CBC' },
|
||||
false,
|
||||
['encrypt']
|
||||
);
|
||||
|
||||
const encrypted = await crypto.subtle.encrypt(
|
||||
{ name: 'AES-CBC', iv: iv },
|
||||
cryptoKey,
|
||||
data
|
||||
);
|
||||
|
||||
return { iv, encrypted: new Uint8Array(encrypted) };
|
||||
};
|
||||
|
||||
const decryptUrl = async (key: Uint8Array, iv: Uint8Array, ciphertext: Uint8Array) => {
|
||||
const cryptoKey = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
key,
|
||||
{ name: 'AES-CBC' },
|
||||
false,
|
||||
['decrypt']
|
||||
);
|
||||
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{ name: 'AES-CBC', iv: iv },
|
||||
cryptoKey,
|
||||
ciphertext
|
||||
);
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
return decoder.decode(decrypted);
|
||||
};
|
||||
|
||||
const arrayToHex = (array: Uint8Array) => {
|
||||
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
|
||||
};
|
||||
|
||||
const hexToArray = (hex: string) => {
|
||||
return new Uint8Array(hex.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16)));
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
const [shortened, setShortened] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isDecrypting, setIsDecrypting] = useState(false);
|
||||
|
||||
// Check for hash in URL on component mount for decryption
|
||||
useEffect(() => {
|
||||
const handleDecryption = async () => {
|
||||
const hash = window.location.hash.slice(1); // Remove # character
|
||||
if (hash && hash.length === 10) {
|
||||
setIsDecrypting(true);
|
||||
try {
|
||||
// Allow UI to update before starting heavy computation
|
||||
await new Promise(resolve => setTimeout(resolve, 20));
|
||||
|
||||
// Hash ID directly for lookup (no salt)
|
||||
const lookupKey = hashIdForLookup(hash);
|
||||
const lookupHash = arrayToHex(lookupKey);
|
||||
|
||||
// Get encrypted data from server
|
||||
const response = await axios.get(`http://tnyr.me/get-encrypted-url?lookup_hash=${lookupHash}`);
|
||||
const { ENCRYTION_SALT, IV, ENCRYPTED_URL } = response.data;
|
||||
|
||||
// Derive decryption key using the encryption salt
|
||||
const encryptionSalt = hexToArray(ENCRYTION_SALT);
|
||||
|
||||
// Allow UI to stay responsive during heavy computation
|
||||
await new Promise(resolve => setTimeout(resolve, 15));
|
||||
|
||||
const decryptionKey = deriveEncryptionKey(hash, encryptionSalt);
|
||||
|
||||
// Decrypt URL
|
||||
const iv = hexToArray(IV);
|
||||
const encryptedUrl = hexToArray(ENCRYPTED_URL);
|
||||
const decryptedUrl = await decryptUrl(decryptionKey, iv, encryptedUrl);
|
||||
|
||||
// Redirect to decrypted URL
|
||||
window.location.href = decryptedUrl;
|
||||
} catch (error) {
|
||||
console.error('Failed to decrypt URL:', error);
|
||||
setIsDecrypting(false);
|
||||
// Could show an error message to user here
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleDecryption();
|
||||
}, []);
|
||||
|
||||
const {
|
||||
register,
|
||||
@@ -47,13 +182,41 @@ export default function App() {
|
||||
clearErrors();
|
||||
|
||||
try {
|
||||
const response = await axios.post("/shorten", {
|
||||
url: data.url,
|
||||
// Allow UI to update before starting heavy computation
|
||||
await new Promise(resolve => setTimeout(resolve, 20));
|
||||
|
||||
// Generate random values
|
||||
const linkId = generateRandomString(10);
|
||||
const encryptionSalt = generateRandomBytes(16);
|
||||
|
||||
// Derive keys
|
||||
const lookupKey = hashIdForLookup(linkId); // Hash ID directly for lookup
|
||||
|
||||
// Allow UI to stay responsive during second hash computation
|
||||
await new Promise(resolve => setTimeout(resolve, 15));
|
||||
|
||||
const encryptionKey = deriveEncryptionKey(linkId, encryptionSalt); // Use random salt for encryption
|
||||
|
||||
// Encrypt URL
|
||||
let url = data.url;
|
||||
if (!url.startsWith('https://') && !url.startsWith('http://') && !url.startsWith('magnet:')) {
|
||||
url = 'http://' + url;
|
||||
}
|
||||
|
||||
const { iv, encrypted } = await encryptUrl(encryptionKey, url);
|
||||
|
||||
// Send to server
|
||||
await axios.post("http://tnyr.me/shorten", {
|
||||
LOOKUP_HASH: arrayToHex(lookupKey),
|
||||
ENCRYTION_SALT: arrayToHex(encryptionSalt),
|
||||
IV: arrayToHex(iv),
|
||||
ENCRYPTED_URL: arrayToHex(encrypted)
|
||||
});
|
||||
const shortUrl = `tnyr.me/${response.data.id}`;
|
||||
|
||||
const shortUrl = `tnyr.me/#${linkId}`;
|
||||
setShortened(shortUrl);
|
||||
} catch {
|
||||
// Simplified error handling - all errors go to root
|
||||
} catch (error) {
|
||||
console.error('Encryption error:', error);
|
||||
setError("root", { message: "Error shortening URL. Please try again." });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -64,6 +227,34 @@ export default function App() {
|
||||
navigator.clipboard.writeText(shortened);
|
||||
};
|
||||
|
||||
// Decryption loading screen
|
||||
if (isDecrypting) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800 text-slate-100 flex flex-col items-center justify-center p-4">
|
||||
<div className="text-center space-y-8">
|
||||
<div className="flex justify-center">
|
||||
<div className="relative">
|
||||
<Loader2 className="w-16 h-16 text-indigo-400 animate-spin" />
|
||||
<div className="absolute inset-0 w-16 h-16 border-4 border-indigo-400/20 rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-3xl font-bold">Decrypting URL</h1>
|
||||
<p className="text-slate-400 text-lg max-w-md">
|
||||
Computing hashes and securely retrieving your destination...
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center gap-2 text-slate-500">
|
||||
<Lock className="w-4 h-4" />
|
||||
<span className="text-sm">End-to-end encrypted</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800 text-slate-100 flex flex-col items-center justify-center p-4">
|
||||
<div className="w-full max-w-2xl space-y-8">
|
||||
@@ -79,8 +270,7 @@ export default function App() {
|
||||
<div className="flex items-center gap-2 justify-center">
|
||||
<Lock className="w-5 h-5" />
|
||||
<p className="text-center">
|
||||
Your links are encrypted - we can't see your destination URLs or
|
||||
share your links!
|
||||
Your links are end-to-end encrypted - your original URL never leaves your browser unencrypted.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -149,32 +339,14 @@ export default function App() {
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium mb-1">
|
||||
Zero-Knowledge Encryption
|
||||
End-to-End Encryption
|
||||
</h3>
|
||||
<p className="text-slate-400 text-sm">
|
||||
Your URL is encrypted using AES-256 with a key derived from
|
||||
your unique link ID. Not even we can decrypt or view your
|
||||
original URL.
|
||||
Your URL is encrypted in your browser and never sent to our servers in plaintext. Using AES-256 encryption, only you and those you share the link with can see the destination.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<div className="mt-1">
|
||||
<Key className="w-5 h-5 text-indigo-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium mb-1">Secure Storage</h3>
|
||||
<p className="text-slate-400 text-sm">
|
||||
We generate two separate hashes - one for identification and
|
||||
another for encrypting the destination. Without the exact
|
||||
ID, the link is completely inaccessible.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-3">
|
||||
<div className="mt-1">
|
||||
<Hash className="w-5 h-5 text-indigo-400" />
|
||||
@@ -187,6 +359,20 @@ export default function App() {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-3">
|
||||
<div className="mt-1">
|
||||
<Key className="w-5 h-5 text-indigo-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium mb-1">Secure By Design</h3>
|
||||
<p className="text-slate-400 text-sm">
|
||||
We derive two separate keys from your link ID. One is used to create a lookup hash, so we can find your encrypted data. The other is used to encrypt your destination URL. Without the original link ID, the data is just random noise.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<div className="mt-1">
|
||||
|
||||
Reference in New Issue
Block a user