diff --git a/app.py b/app.py new file mode 100644 index 0000000..a14fbb5 --- /dev/null +++ b/app.py @@ -0,0 +1,88 @@ +from flask import Flask, render_template, request, jsonify +import html +import os +import base64 +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC +from cryptography.hazmat.primitives.hashes import SHA256 +from cryptography.hazmat.primitives.ciphers.aead import AESGCM +from waitress import serve + +app = Flask(__name__) + +# Basic Encoder/Decoder +ALPHABET = list('abcdefghijklmnopqrstuvwxyz') + +def simple_encode(text: str) -> str: + return ''.join( + ALPHABET[(ALPHABET.index(c) + 3) % 26] if c in ALPHABET else c + for c in text.lower() + ) + +def simple_decode(text: str) -> str: + return ''.join( + ALPHABET[(ALPHABET.index(c) - 3) % 26] if c in ALPHABET else c + for c in text.lower() + ) + +# Advanced Encrypt/Decrypt using AES-GCM +def derive_key(password: str, salt: bytes) -> bytes: + kdf = PBKDF2HMAC( + algorithm=SHA256(), + length=32, + salt=salt, + iterations=200_000, + ) + return kdf.derive(password.encode()) + +def advanced_encrypt(plaintext: str, password: str) -> str: + salt = os.urandom(16) + key = derive_key(password, salt) + + aesgcm = AESGCM(key) + nonce = os.urandom(12) + + ct = aesgcm.encrypt(nonce, plaintext.encode(), None) + encrypted = salt + nonce + ct + return base64.urlsafe_b64encode(encrypted).decode() + +def advanced_decrypt(token_b64: str, password: str) -> str: + try: + data = base64.urlsafe_b64decode(token_b64.encode()) + salt, nonce, ct = data[:16], data[16:28], data[28:] + key = derive_key(password, salt) + aesgcm = AESGCM(key) + pt = aesgcm.decrypt(nonce, ct, None) + return pt.decode() + except Exception: + return "[Error] Invalid password or corrupted data!" + +# Combined Route for Page & AJAX +@app.route("/", methods=["GET", "POST"]) +def index(): + if request.method == 'POST': + data = request.get_json() + encryption_type = data.get("encryption-type", "basic") + operation = data.get("operation", "") + message = data.get("message", "") + password = data.get("password", "") + file_password = data.get("file-password", "") + + final_password = file_password if file_password else password + + if encryption_type == "basic": + result = simple_encode(message) if operation == "encrypt" else simple_decode(message) + else: + result = advanced_encrypt(message, final_password) if operation == "encrypt" else advanced_decrypt(message, final_password) + + return jsonify(result=html.escape(result)) + + return render_template( + "index.html", + result="", + password="", + encryption_type="advanced" + ) + +if __name__ == "__main__": + # Use Waitress to serve the app in production + serve(app, host="0.0.0.0", port=5000) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1d89f7f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +### **requirements.txt** + +Flask==2.1.2 +cryptography==3.4.8 +nginx==1.21.0 # Only needed for Nginx integration, not installed via pip \ No newline at end of file diff --git a/static/css/styles.css b/static/css/styles.css new file mode 100644 index 0000000..5e32031 --- /dev/null +++ b/static/css/styles.css @@ -0,0 +1,311 @@ +/* ===== Global Reset ===== */ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +/* ===== Body ===== */ +body { + font-family: 'Poppins', sans-serif; + background-color: #121212; + color: #f0f0f0; + display: flex; + flex-direction: column; + min-height: 100vh; + justify-content: center; /* Vertically center content */ + align-items: center; /* Horizontally center content */ + padding: 20px; +} + +/* ===== Header ===== */ +header { + text-align: center; + padding: 30px 20px; + background-color: #1c1c1c; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.5); + width: 100%; + max-width: 800px; +} + + header h1 { + font-size: 2.8em; + color: #00ff99; + margin-bottom: 8px; + } + + header p { + font-size: 1.1em; + color: #00ff99; + } + +/* ===== Main Layout ===== */ +main { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + max-width: 800px; + padding: 20px; + gap: 30px; + justify-content: center; +} + +/* ===== Section Card Styling ===== */ +.card { + background-color: #1e1e1e; + padding: 20px 25px; + width: 100%; + max-width: 800px; + border-radius: 12px; + box-shadow: 0 0 15px rgba(0, 255, 153, 0.4); + text-align: center; +} + +/* ===== Uniform Form Inputs ===== */ +.form-group { + display: flex; + flex-direction: column; + align-items: center; + gap: 15px; + width: 100%; +} + +input, +textarea, +select, +input[type="file"] { + width: 80%; + max-width: 500px; + padding: 12px 20px; + border: 2px solid #00ff99; + border-radius: 8px; + background-color: #2c2f33; + color: #00ff99; + font-size: 1em; + transition: 0.3s; +} + +textarea { + min-height: 140px; + resize: none; +} + +input[type="password"] { + min-height: 50px; +} + +input[type="file"] { + border: 2px dashed #00ff99; + cursor: pointer; + text-align: center; +} + + input[type="file"]::file-selector-button { + background-color: #00ff99; + color: #121212; + border: none; + padding: 8px 15px; + border-radius: 6px; + cursor: pointer; + transition: 0.3s; + } + + input[type="file"]::file-selector-button:hover { + background-color: #00cc77; + } + +input:focus, +textarea:focus, +select:focus { + outline: none; + box-shadow: 0 0 8px rgba(0, 255, 153, 0.8); +} + +/* ===== Match input and output textarea sizes ===== */ +#input-text, +#output-text { + width: 80%; + max-width: 500px; + height: 140px; + box-sizing: border-box; + resize: none; +} + +/* ===== Buttons ===== */ +.button-group { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 8px; + margin-top: 8px; + width: 100%; +} + +button { + padding: 10px 20px; + border: 2px solid #00ff99; + border-radius: 8px; + background-color: #2c2f33; + color: #00ff99; + font-size: 1em; + cursor: pointer; + transition: 0.3s; + width: 100%; /* Makes buttons stretch to fill container */ + max-width: 200px; /* Restricts button width */ +} + + button:hover { + background-color: #00ff99; + color: #121212; + } + +/* ===== Toggle Buttons ===== */ +.radio-group { + display: flex; + justify-content: center; + gap: 8px; + margin-top: 8px; + width: 100%; +} + +.radio-button { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 8px 18px; + border: 2px solid #00ff99; + border-radius: 8px; + background-color: #2c2f33; + color: #00ff99; + cursor: pointer; + transition: 0.3s; +} + + .radio-button:hover { + background-color: #00ff99; + color: #121212; + } + + .radio-button input { + display: none; + } + + .radio-button input:checked + span { + background-color: #00ff99; + color: #121212; + padding: 8px 18px; + border-radius: 8px; + display: inline-block; + } + +/* ===== Remove File Button ===== */ +#remove-file-btn { + display: none; /* only shows when a file is selected */ + margin-top: 8px; + padding: 8px 16px; + border: 2px solid #ff5555; + background-color: #2c2f33; + color: #ff5555; + border-radius: 8px; + cursor: pointer; + transition: 0.3s; +} + + #remove-file-btn:hover { + background-color: #ff5555; + color: #2c2f33; + } + +/* ===== Toast Notifications ===== */ +.toast { + visibility: hidden; + min-width: 250px; + background-color: #333; + color: #00ff99; + text-align: center; + border-radius: 6px; + padding: 10px; + margin-top: 8px; + font-size: 0.9em; + animation: fadein 0.5s, fadeout 0.5s 2.5s; +} + + .toast.show { + visibility: visible; + } + +@keyframes fadein { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +@keyframes fadeout { + from { + opacity: 1; + } + + to { + opacity: 0; + } +} + +/* ===== Pacman Canvas ===== */ +.pacman-wrapper { + display: flex; + justify-content: center; + margin-bottom: 18px; +} + +#pacmanCanvas { + background-color: black; + border: 2px solid #00ff99; + border-radius: 10px; + width: 800px; + height: 600px; +} + +/* ===== Footer ===== */ +footer { + text-align: center; + padding: 18px; + background-color: #1c1c1c; + color: #00ff99; + margin-top: auto; + width: 100%; +} + + footer a { + color: #00ff99; + text-decoration: none; + } + + footer a:hover { + color: #ff0066; + } + +/* ===== Password Input Field ===== */ +#password-input { + display: flex; /* Password input is visible by default */ + margin-top: 15px; + flex-direction: column; + gap: 10px; + width: 100%; + max-width: 500px; +} + + #password-input input { + padding: 12px; + font-size: 1em; + border: 2px solid #00ff99; + border-radius: 8px; + background-color: #2c2f33; + color: #00ff99; + width: 100%; /* Ensure the password field takes full width */ + } diff --git a/static/img/PacCrypt.png b/static/img/PacCrypt.png new file mode 100644 index 0000000..2338743 Binary files /dev/null and b/static/img/PacCrypt.png differ diff --git a/static/js/script.js b/static/js/script.js new file mode 100644 index 0000000..2785547 --- /dev/null +++ b/static/js/script.js @@ -0,0 +1,496 @@ +// ===== AES Encryption ===== +async function encryptAdvanced(message, password) { + // Create a random salt for key derivation + const salt = crypto.getRandomValues(new Uint8Array(16)); + + // Derive a key from the password using PBKDF2 and the salt + const key = await deriveKey(password, salt); + + // Create a random initialization vector (IV) + const iv = crypto.getRandomValues(new Uint8Array(12)); + + // Encode the message as a Uint8Array + const encoder = new TextEncoder(); + const encodedMessage = encoder.encode(message); + + // Encrypt the message using AES-GCM + const encryptedMessage = await crypto.subtle.encrypt( + { name: 'AES-GCM', iv: iv }, + key, + encodedMessage + ); + + // Combine salt, IV, and encrypted message + const encryptedArray = new Uint8Array(salt.length + iv.length + encryptedMessage.byteLength); + encryptedArray.set(salt); + encryptedArray.set(iv, salt.length); + encryptedArray.set(new Uint8Array(encryptedMessage), salt.length + iv.length); + + // Convert the result to base64 to send to the server + return btoa(String.fromCharCode.apply(null, encryptedArray)); +} + +// Derive a key from the password using PBKDF2 +async function deriveKey(password, salt) { + const encoder = new TextEncoder(); + const passwordBuffer = encoder.encode(password); + + const key = await crypto.subtle.importKey( + 'raw', + passwordBuffer, + { name: 'PBKDF2' }, + false, + ['deriveKey'] + ); + + return crypto.subtle.deriveKey( + { + name: 'PBKDF2', + salt: salt, + iterations: 100000, + hash: 'SHA-256', + }, + key, + { name: 'AES-GCM', length: 256 }, + false, + ['encrypt', 'decrypt'] + ); +} + +// ===== AES Decryption ===== +async function decryptAdvanced(encryptedData, password) { + // Decode the base64-encoded encrypted data + const encryptedArray = new Uint8Array(atob(encryptedData).split("").map(char => char.charCodeAt(0))); + + // Extract salt, IV, and encrypted message from the encrypted data + const salt = encryptedArray.slice(0, 16); + const iv = encryptedArray.slice(16, 28); + const encryptedMessage = encryptedArray.slice(28); + + // Derive the key from the password and salt + const key = await deriveKey(password, salt); + + // Decrypt the message using AES-GCM + const decryptedMessage = await crypto.subtle.decrypt( + { name: 'AES-GCM', iv: iv }, + key, + encryptedMessage + ); + + // Decode the decrypted message to text + const decoder = new TextDecoder(); + return decoder.decode(decryptedMessage); +} + +// ===== UI Toggles ===== +function toggleEncryptionOptions() { + const type = document.getElementById("encryption-type").value; + const pwdContainer = document.getElementById("password-input"); + pwdContainer.style.display = (type === 'advanced') ? 'flex' : 'none'; + if (type === 'basic') removeFile(); + toggleInputMode(); + document.getElementById("encrypt-label").textContent = + (type === 'basic') ? "Encode" : "Encrypt"; + document.getElementById("decrypt-label").textContent = + (type === 'basic') ? "Decode" : "Decrypt"; +} + +// ===== Remove File Button ===== +function removeFile() { + document.getElementById("file-input").value = ""; // Clear the file input + document.getElementById("remove-file-btn").style.display = 'none'; // Hide the remove file button + toggleInputMode(); // Reapply the input mode logic + document.getElementById("file-password-input").style.display = 'none'; // Hide the file password input +} + +// ===== Input vs. File Toggle ===== +function toggleInputMode() { + const textValue = document.getElementById("input-text").value.trim(); + const fileSelected = document.getElementById("file-input").files.length > 0; + const isAdvanced = document.getElementById("encryption-type").value === 'advanced'; + + // Show/hide text area based on file selection + document.getElementById("text-section").style.display = + fileSelected ? 'none' : 'flex'; + + // Show/hide file input section when in advanced mode and no text input is given + document.getElementById("file-section").style.display = + (isAdvanced && !textValue) ? 'flex' : 'none'; + + // Show/hide the remove file button + document.getElementById("remove-file-btn").style.display = + fileSelected ? 'inline-block' : 'none'; + + // ALWAYS show the password input in advanced mode + if (isAdvanced) { + document.getElementById("password-input").style.display = 'flex'; + } else { + document.getElementById("password-input").style.display = 'none'; + } + + // Show the dedicated password input for file encryption if a file is selected + if (fileSelected) { + document.getElementById("file-password-input").style.display = 'flex'; // Show password input for files + } else { + document.getElementById("file-password-input").style.display = 'none'; // Hide when no file is selected + } +} + +// ===== Validate and Submit Form ===== +async function handleSubmit(event) { + event.preventDefault(); + + // If the encryption type is advanced, ensure password is provided + const password = document.getElementById("password").value; + const filePassword = document.getElementById("file-password") ? document.getElementById("file-password").value : ''; + const encryptionType = document.getElementById("encryption-type").value; + + if (encryptionType === 'advanced' && !password && !filePassword) { + alert("Password is required for advanced encryption."); + return; + } + + // Prepare the form data + const payload = { + "encryption-type": encryptionType, + operation: document.querySelector('input[name="operation"]:checked').value, + message: document.getElementById("input-text").value, + password: password, + "file-password": filePassword + }; + + // Handle file upload encryption/decryption + const fileInput = document.getElementById("file-input"); + if (fileInput.files.length > 0) { + const op = document.querySelector('input[name="operation"]:checked').value; + if (op === 'encrypt') encryptFile(); + else decryptFile(); + return; + } + + // Handle text encryption/decryption + try { + const resp = await fetch("/", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload) + }); + const data = await resp.json(); + document.getElementById("output-text").value = data.result; + } catch (err) { + alert("Error processing request: " + err); + } +} + +// ===== File Encryption / Decryption ===== +function encryptFile() { + const f = document.getElementById("file-input"); + const pwd = document.getElementById("file-password").value; + if (!pwd) return alert("Please enter a password!"); + if (!f.files.length) return alert("Please select a file!"); + const reader = new FileReader(); + reader.onload = async (e) => { + const raw = e.target.result; + let encryptedMessage = await encryptAdvanced(raw, pwd); + downloadFile(encryptedMessage, f.files[0].name + ".enc"); + }; + reader.readAsText(f.files[0]); +} + +function decryptFile() { + const f = document.getElementById("file-input"); + const pwd = document.getElementById("file-password").value; + if (!pwd) return alert("Please enter a password!"); + if (!f.files.length) return alert("Please select a file!"); + const reader = new FileReader(); + reader.onload = async (e) => { + try { + const enc = e.target.result; + const decryptedMessage = await decryptAdvanced(enc, pwd); + downloadFile(decryptedMessage, f.files[0].name.replace(/\.enc$/, '')); + } catch { + alert("Decryption failed: wrong password or corrupted file."); + } + }; + reader.readAsText(f.files[0]); +} + +function downloadFile(content, filename) { + const blob = new Blob([content], { type: "application/octet-stream" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); +} + +// ===== Password Generator ===== +function generateRandomPassword() { + const length = 30; + const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+[]{}|;:,.<>?/~"; + let password = ""; + for (let i = 0; i < length; i++) { + password += charset.charAt(Math.floor(Math.random() * charset.length)); + } + document.getElementById("password-field").value = password; +} + +// ===== Copy to Clipboard ===== +function copyToClipboard(elementId, toastId) { + const copyText = document.getElementById(elementId); + copyText.select(); + copyText.setSelectionRange(0, 99999); // For mobile devices + document.execCommand("copy"); + + // Show toast notification + const toast = document.getElementById(toastId); + toast.classList.add("show"); + setTimeout(() => toast.classList.remove("show"), 2000); // Remove toast after 2 seconds +} + +// ===== Pacman Easter Egg ===== +function checkForPacman() { + const val = document.getElementById("input-text").value.trim().toLowerCase(); + const pacSection = document.getElementById("pacman-section"); + const encSection = document.getElementById("encoding-section"); + + if (val.includes('pacman') && pacSection.style.display !== 'block') { + pacSection.style.display = 'block'; + encSection.style.display = 'none'; + startPacman(); + } else if (pacSection.style.display === 'block' && !val.includes('pacman')) { + exitGame(); + } +} + +// ===== Game Exit & Restart ===== +function exitGame() { + stopPacman(); + document.getElementById("input-text").value = ""; + document.getElementById("pacman-section").style.display = 'none'; + document.getElementById("encoding-section").style.display = 'block'; +} + +function resetGame() { + stopPacman(); + startPacman(); +} + +// ===== Pacman Game Variables & Logic ===== +let canvas, ctx, pacman, enemy, walls, dots, score; +let pacmanSpeed = 40, enemySpeed = 20, cellSize = 40, dotSize = 5; +let cols, rows, randSeed, gameInterval; + +function startPacman() { + canvas = document.getElementById("pacmanCanvas"); + ctx = canvas.getContext("2d"); + cols = Math.floor(canvas.width / cellSize); + rows = Math.floor(canvas.height / cellSize); + walls = []; dots = []; score = 0; + clearInterval(gameInterval); + + randSeed = Array.from( + document.getElementById("password-field").value + ).reduce((s, c) => s + c.charCodeAt(0), 0); + + generateWalls(); + generateDots(); + + pacman = spawn(); + do { + enemy = spawn(); + } while (enemy.x === pacman.x && enemy.y === pacman.y); + + pacman.dx = pacman.dy = 0; + document.addEventListener("keydown", movePacman); + gameInterval = setInterval(gameLoop, 150); +} + +function stopPacman() { + clearInterval(gameInterval); + if (ctx) ctx.clearRect(0, 0, canvas.width, canvas.height); +} + +function spawn() { + const opts = []; + for (let c = 1; c < cols - 1; c++) { + for (let r = 1; r < rows - 1; r++) { + if (!walls.some(w => w.c === c && w.r === r)) { + const neighbors = [ + { c: c+1, r }, { c: c-1, r }, + { c, r: r+1 }, { c, r: r-1 } + ]; + if (neighbors.some(n => + !walls.some(w => w.c===n.c && w.r===n.r) + )) { + opts.push({ c, r }); + } + } + } + } + const s = opts[Math.floor(rand() * opts.length)]; + return { + x: s.c * cellSize + cellSize/2, + y: s.r * cellSize + cellSize/2, + size: cellSize/2 - 5, + dx: 0, + dy: 0 + }; +} + +function rand() { + const x = Math.sin(randSeed++) * 10000; + return x - Math.floor(x); +} + +function generateWalls() { + for (let c = 0; c < cols; c++) { + for (let r = 0; r < rows; r++) { + if (c===0||r===0||c===cols-1||r===rows-1||rand()<0.2) { + walls.push({ c, r }); + } + } + } +} + +function generateDots() { + dots = []; + for (let c = 1; c < cols - 1; c++) { + for (let r = 1; r < rows - 1; r++) { + if (walls.some(w => w.c===c && w.r===r)) continue; + const isEnclosed = + walls.some(w => w.c===c+1 && w.r===r) && + walls.some(w => w.c===c-1 && w.r===r) && + walls.some(w => w.c===c && w.r===r+1) && + walls.some(w => w.c===c && w.r===r-1); + if (!isEnclosed) dots.push({ c, r }); + } + } +} + +function movePacman(e) { + if (!["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].includes(e.key)) return; + e.preventDefault(); + if (e.key==="ArrowUp") { pacman.dx=0; pacman.dy=-pacmanSpeed; } + if (e.key==="ArrowDown") { pacman.dx=0; pacman.dy=pacmanSpeed; } + if (e.key==="ArrowLeft") { pacman.dx=-pacmanSpeed; pacman.dy=0; } + if (e.key==="ArrowRight") { pacman.dx=pacmanSpeed; pacman.dy=0; } +} + +// ===== Collision Helper ===== +function willCollide(x, y, size) { + const left = x - size, right = x + size; + const top = y - size, bottom = y + size; + for (let w of walls) { + const wx1 = w.c * cellSize, wy1 = w.r * cellSize; + const wx2 = wx1 + cellSize, wy2 = wy1 + cellSize; + if (right > wx1 && left < wx2 && bottom > wy1 && top < wy2) { + return true; + } + } + return false; +} + +function moveChar(ch) { + const nx = ch.x + ch.dx, ny = ch.y + ch.dy; + if (!willCollide(nx, ny, ch.size)) { + ch.x = nx; ch.y = ny; + } +} + +function moveEnemy() { + const options = []; + [[enemySpeed,0],[-enemySpeed,0],[0,enemySpeed],[0,-enemySpeed]].forEach( + ([dx,dy]) => { + const nx = enemy.x + dx, ny = enemy.y + dy; + if (!willCollide(nx, ny, enemy.size)) options.push({dx,dy}); + } + ); + if (!options.length) return; + let best = options[0]; + let bestD = Math.abs(enemy.x+best.dx-pacman.x)+Math.abs(enemy.y+best.dy-pacman.y); + for (let opt of options) { + const d = Math.abs(enemy.x+opt.dx-pacman.x)+Math.abs(enemy.y+opt.dy-pacman.y); + if (d < bestD) { best=opt; bestD=d; } + } + enemy.x += best.dx; enemy.y += best.dy; +} + +function gameLoop() { + ctx.clearRect(0,0,canvas.width,canvas.height); + drawWalls(); + moveChar(pacman); + moveEnemy(); + drawChar(pacman,"yellow"); + drawChar(enemy,"red"); + eatDots(); + drawScore(); + checkGameOver(); +} + +function drawWalls() { + ctx.fillStyle="blue"; + walls.forEach(w=>ctx.fillRect(w.c*cellSize,w.r*cellSize,cellSize,cellSize)); +} + +function drawChar(ch,color) { + ctx.beginPath(); + ctx.arc(ch.x,ch.y,ch.size,0,Math.PI*2); + ctx.fillStyle=color; ctx.fill(); +} + +function eatDots() { + dots = dots.filter(d=>{ + const dx = d.c*cellSize+cellSize/2, dy = d.r*cellSize+cellSize/2; + if (Math.abs(pacman.x-dx){ + ctx.beginPath(); + ctx.arc(d.c*cellSize+cellSize/2, d.r*cellSize+cellSize/2, dotSize,0,Math.PI*2); + ctx.fill(); + }); +} + +function drawScore() { + ctx.fillStyle="white"; + ctx.font="20px Poppins"; + ctx.fillText("Score: "+score,10,25); +} + +function checkGameOver() { + if (Math.abs(pacman.x-enemy.x) { + toggleEncryptionOptions(); + toggleInputMode(); + document.getElementById("input-text").addEventListener("input", checkForPacman); +}); diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..88a2754 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,112 @@ + + + + + + PacCrypt + + + + + + + + +
+

PacCrypt

+

Secure Encoding, Encryption and Password Generation

+
+ +
+
+

Password Generator

+
+ +
+ + +
+
Copied to Clipboard!
+
+
+ + + +
+

Text Encoder / Decoder & File Encryption

+
+ + + +
+ + +
+ +
+ +
+ +
+
+ + + + + + +
+ +
+ + +
+ + +
+
Copied to Clipboard!
+
+
+ + + +