mirror of
https://codeberg.org/theamazing0/portfolio.git
synced 2025-10-30 10:22:18 -04:00
Added POW before viewing contact info
This commit is contained in:
parent
6f303a0f03
commit
2cce03ecf3
4 changed files with 248 additions and 175 deletions
|
|
@ -1,12 +1,12 @@
|
|||
---
|
||||
interface Props {
|
||||
medium: string;
|
||||
id: string;
|
||||
link: string;
|
||||
// id: string;
|
||||
// link: string;
|
||||
pgp?: boolean;
|
||||
}
|
||||
|
||||
const { medium, id, link, pgp } = Astro.props;
|
||||
const { medium, pgp } = Astro.props;
|
||||
|
||||
import { Icon } from "astro-icon/components";
|
||||
|
||||
|
|
@ -17,7 +17,7 @@ const icons: { [key: string]: string } = {
|
|||
};
|
||||
---
|
||||
|
||||
<div class="main" x-data="{id: '', link: ''}">
|
||||
<div class="main" data-contact-card>
|
||||
<div style="display:flex">
|
||||
<div
|
||||
style="display: flex; align-items: center; margin-right: 0.2rem; margin-left: 1.75rem;"
|
||||
|
|
@ -25,9 +25,9 @@ const icons: { [key: string]: string } = {
|
|||
<Icon name={icons[medium.toLowerCase()]} size="50" />
|
||||
</div>
|
||||
<div style="margin-left:1rem; margin-bottom: 1rem" class="cardText">
|
||||
<h2>{medium}</h2>
|
||||
<h2 data-medium>{medium}</h2>
|
||||
<div>
|
||||
<p style="margin-bottom: 0.25rem;" x-text="id"></p>
|
||||
<p style="margin-bottom: 0.25rem;" data-id></p>
|
||||
{
|
||||
pgp ? (
|
||||
<a class="pgp" href="/">
|
||||
|
|
@ -37,7 +37,7 @@ const icons: { [key: string]: string } = {
|
|||
}
|
||||
</div>
|
||||
<div style={pgp ? "padding-top: 1rem" : "padding-top: 2rem"}>
|
||||
<a x-bind:href="link">Open -></a>
|
||||
<a data-link>Open -></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -83,13 +83,13 @@ const icons: { [key: string]: string } = {
|
|||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
#main {
|
||||
.main {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 769px) {
|
||||
#main {
|
||||
.main {
|
||||
width: 18.5rem;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
236
src/components/EncryptedContactView.astro
Normal file
236
src/components/EncryptedContactView.astro
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
---
|
||||
import ContactCard from "../components/ContactCard.astro";
|
||||
|
||||
import crypto from "crypto";
|
||||
|
||||
// Store contact info in a dictionary
|
||||
const contactInfo = {
|
||||
email: {
|
||||
value: "me@samvid.dev",
|
||||
link: "mailto:hi@samvid.dev",
|
||||
},
|
||||
signal: {
|
||||
value: "samvidk.85",
|
||||
link: "https://signal.me/#eu/eFj_OirfNWU7IkLM_0rHhusUBVuQQaoPGYH_1LrhdmSKuW-TIflE6ovHlxGkB-Vq",
|
||||
},
|
||||
matrix: {
|
||||
value: "@theamazing0:tchncs.de",
|
||||
link: "https://matrix.to/#/@theamazing0:tchncs.de",
|
||||
},
|
||||
};
|
||||
|
||||
const encryptedContactInfo = JSON.stringify(
|
||||
encryptString(JSON.stringify(contactInfo)),
|
||||
);
|
||||
|
||||
function encryptString(input: string, difficultyPrefix = "000") {
|
||||
const challenge = crypto.randomBytes(8).toString("hex");
|
||||
let nonce = 0;
|
||||
let hash;
|
||||
|
||||
while (true) {
|
||||
const candidate = challenge + nonce;
|
||||
hash = crypto.createHash("sha256").update(candidate).digest("hex");
|
||||
if (hash.startsWith(difficultyPrefix)) break;
|
||||
nonce++;
|
||||
}
|
||||
|
||||
const key = crypto
|
||||
.createHash("sha256")
|
||||
.update(challenge + nonce)
|
||||
.digest()
|
||||
.slice(0, 32);
|
||||
|
||||
const iv = crypto.randomBytes(12);
|
||||
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
||||
const encrypted = Buffer.concat([
|
||||
cipher.update(input, "utf8"),
|
||||
cipher.final(),
|
||||
]);
|
||||
const tag = cipher.getAuthTag();
|
||||
|
||||
return {
|
||||
ciphertext: Buffer.concat([encrypted, tag]).toString("base64"),
|
||||
iv: iv.toString("base64"),
|
||||
challenge,
|
||||
difficulty: difficultyPrefix,
|
||||
};
|
||||
}
|
||||
---
|
||||
|
||||
<div id="prompt-div">
|
||||
<p class="subtitle">
|
||||
In order to protect against bots, this website uses a lightweight proof of
|
||||
work algorithm in your browser to decrypt my contact information.
|
||||
</p>
|
||||
<p class="subtitle">
|
||||
This keeps spam bots from finding my info while keeping it easy for humans
|
||||
to access it.
|
||||
</p>
|
||||
<p class="subtitle" style="font-weight: 700;" id="progress-label"></p>
|
||||
<a class="button" id="decode-btn">Decode Info</a>
|
||||
</div>
|
||||
<div id="details-div" data-encrypted={encryptedContactInfo} hidden>
|
||||
<p class="subtitle">I would love to talk to you!</p>
|
||||
<p class="subtitle">
|
||||
The following methods support <span style="font-weight: 700;"
|
||||
>privacy-friendly</span
|
||||
> and <span style="font-weight: 700;">secure</span> communication:
|
||||
</p>
|
||||
<div class="card-container">
|
||||
<ContactCard medium="Email" pgp />
|
||||
<ContactCard medium="Signal" />
|
||||
<ContactCard medium="Matrix" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.subtitle {
|
||||
color: #c3d6b2;
|
||||
}
|
||||
.card-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.button {
|
||||
display: inline-block;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.8rem;
|
||||
background-color: #e6fbd4;
|
||||
color: #222b1a;
|
||||
font-weight: 700;
|
||||
border-radius: 10px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 0.2s,
|
||||
color 0.2s;
|
||||
}
|
||||
.button:hover {
|
||||
background-color: #c3d6b2;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 769px) {
|
||||
.subtitle {
|
||||
max-width: 65%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
async function sha256(str: string) {
|
||||
const buf = new TextEncoder().encode(str);
|
||||
const hash = await crypto.subtle.digest("SHA-256", buf);
|
||||
return Array.from(new Uint8Array(hash))
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
}
|
||||
|
||||
async function findNonce(challenge: string, difficultyPrefix = "00000") {
|
||||
let nonce = 0;
|
||||
const startTime = performance.now();
|
||||
let lastUpdateTime = startTime;
|
||||
|
||||
while (true) {
|
||||
const hash = await sha256(challenge + nonce);
|
||||
if (hash.startsWith(difficultyPrefix)) {
|
||||
return nonce.toString();
|
||||
}
|
||||
nonce++;
|
||||
|
||||
const currentTime = performance.now();
|
||||
if (currentTime - lastUpdateTime >= 1000) {
|
||||
const elapsedTime = (currentTime - startTime) / 1000;
|
||||
const attemptsPerSecond = Math.floor(nonce / elapsedTime);
|
||||
const estimatedTotalAttempts = Math.pow(16, difficultyPrefix.length);
|
||||
const progress = Math.min(
|
||||
(nonce / estimatedTotalAttempts) * 100,
|
||||
100,
|
||||
).toFixed(2);
|
||||
const eta = (
|
||||
(estimatedTotalAttempts - nonce) /
|
||||
attemptsPerSecond
|
||||
).toFixed(2);
|
||||
|
||||
const progressLabel = document.querySelector("#progress-label");
|
||||
if (progressLabel) {
|
||||
progressLabel.innerHTML = `~${progress}% done, ${attemptsPerSecond}/s, ETA: ${eta}s`;
|
||||
}
|
||||
|
||||
lastUpdateTime = currentTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function decryptData(e: Event) {
|
||||
e.preventDefault();
|
||||
|
||||
const detailsDiv = document.getElementById("details-div");
|
||||
if (detailsDiv) {
|
||||
const encryptedDetails = detailsDiv.dataset.encrypted;
|
||||
if (encryptedDetails) {
|
||||
const { ciphertext, iv, challenge, difficulty } =
|
||||
JSON.parse(encryptedDetails);
|
||||
|
||||
const nonce = await findNonce(challenge, difficulty);
|
||||
const keyMaterial = await sha256(challenge + nonce);
|
||||
const matchResult = keyMaterial.match(/.{1,2}/g);
|
||||
if (!matchResult) {
|
||||
throw new Error("Failed to match key material");
|
||||
}
|
||||
const keyBytes = new Uint8Array(
|
||||
matchResult.map((b) => parseInt(b, 16)),
|
||||
);
|
||||
const cryptoKey = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
keyBytes,
|
||||
{ name: "AES-GCM" },
|
||||
false,
|
||||
["decrypt"],
|
||||
);
|
||||
const cipherBytes = Uint8Array.from(atob(ciphertext), (c) =>
|
||||
c.charCodeAt(0),
|
||||
);
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{
|
||||
name: "AES-GCM",
|
||||
iv: Uint8Array.from(atob(iv), (c) => c.charCodeAt(0)),
|
||||
},
|
||||
cryptoKey,
|
||||
cipherBytes,
|
||||
);
|
||||
const decryptedData = JSON.parse(new TextDecoder().decode(decrypted));
|
||||
|
||||
document.querySelectorAll("[data-contact-card]").forEach((card) => {
|
||||
const mediumLabel = card.querySelector("[data-medium]");
|
||||
if (mediumLabel) {
|
||||
const medium = mediumLabel.innerHTML.toLowerCase();
|
||||
if (medium) {
|
||||
const idLabel = card.querySelector("[data-id]");
|
||||
if (idLabel) {
|
||||
idLabel.innerHTML = decryptedData[medium]["value"];
|
||||
}
|
||||
const linkBtn = card.querySelector("[data-link]");
|
||||
if (linkBtn) {
|
||||
linkBtn.setAttribute("href", decryptedData[medium]["link"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
const promptDiv = document.getElementById("prompt-div");
|
||||
if (promptDiv) {
|
||||
promptDiv.hidden = true;
|
||||
}
|
||||
const detailsDiv = document.getElementById("details-div");
|
||||
if (detailsDiv) {
|
||||
detailsDiv.hidden = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const decodeBtn = document.getElementById("decode-btn");
|
||||
if (decodeBtn) {
|
||||
decodeBtn.addEventListener("click", decryptData);
|
||||
}
|
||||
</script>
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
@media only screen and (min-width: 769px) {
|
||||
justify-content: unset;
|
||||
border-radius: 8px;
|
||||
border-radius: 10px;
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,91 +1,12 @@
|
|||
---
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
import ContactCard from "../components/ContactCard.astro";
|
||||
|
||||
import crypto from "crypto";
|
||||
|
||||
// Store contact info in a dictionary
|
||||
const contactInfo = {
|
||||
email: {
|
||||
value: "me@samvid.dev",
|
||||
link: "mailto:hi@samvid.dev",
|
||||
},
|
||||
signal: {
|
||||
value: "samvidk.85",
|
||||
link: "https://signal.me/#eu/eFj_OirfNWU7IkLM_0rHhusUBVuQQaoPGYH_1LrhdmSKuW-TIflE6ovHlxGkB-Vq",
|
||||
},
|
||||
matrix: {
|
||||
value: "@theamazing0:tchncs.de",
|
||||
link: "https://matrix.to/#/@theamazing0:tchncs.de",
|
||||
},
|
||||
};
|
||||
|
||||
const encryptedContactInfo = encryptString(JSON.stringify(contactInfo));
|
||||
|
||||
function encryptString(input: string, difficultyPrefix = "000") {
|
||||
const challenge = crypto.randomBytes(8).toString("hex");
|
||||
let nonce = 0;
|
||||
let hash;
|
||||
|
||||
while (true) {
|
||||
const candidate = challenge + nonce;
|
||||
hash = crypto.createHash("sha256").update(candidate).digest("hex");
|
||||
if (hash.startsWith(difficultyPrefix)) break;
|
||||
nonce++;
|
||||
}
|
||||
|
||||
const key = crypto
|
||||
.createHash("sha256")
|
||||
.update(challenge + nonce)
|
||||
.digest()
|
||||
.slice(0, 32);
|
||||
|
||||
const iv = crypto.randomBytes(12);
|
||||
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
||||
const encrypted = Buffer.concat([
|
||||
cipher.update(input, "utf8"),
|
||||
cipher.final(),
|
||||
]);
|
||||
const tag = cipher.getAuthTag();
|
||||
|
||||
return {
|
||||
ciphertext: Buffer.concat([encrypted, tag]).toString("base64"),
|
||||
iv: iv.toString("base64"),
|
||||
challenge,
|
||||
difficulty: difficultyPrefix,
|
||||
};
|
||||
}
|
||||
import EncryptedContactView from "../components/EncryptedContactView.astro";
|
||||
---
|
||||
|
||||
<Layout title="Contact | Samvid Konchada">
|
||||
<div>
|
||||
<h1 class="header">Contact</h1>
|
||||
<div id="details-div" style="display: none;">
|
||||
<p class="subtitle">I would love to talk to you!</p>
|
||||
<p class="subtitle">
|
||||
The following methods support <span style="font-weight: 700;"
|
||||
>privacy-friendly</span
|
||||
> and <span style="font-weight: 700;">secure</span> communication:
|
||||
</p>
|
||||
<div class="card-container">
|
||||
<ContactCard
|
||||
medium="Email"
|
||||
id="me@samvid.dev"
|
||||
link="mailto:hi@samvid.dev"
|
||||
pgp
|
||||
/>
|
||||
<ContactCard
|
||||
medium="Signal"
|
||||
id="samvidk.85"
|
||||
link="https://signal.me/#eu/eFj_OirfNWU7IkLM_0rHhusUBVuQQaoPGYH_1LrhdmSKuW-TIflE6ovHlxGkB-Vq"
|
||||
/>
|
||||
<ContactCard
|
||||
medium="Matrix"
|
||||
id="@theamazing0:tchncs.de"
|
||||
link="https://matrix.to/#/@theamazing0:tchncs.de"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<EncryptedContactView />
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
|
|
@ -96,88 +17,4 @@ function encryptString(input: string, difficultyPrefix = "000") {
|
|||
margin-bottom: 1.5rem;
|
||||
color: #e6fbd4;
|
||||
}
|
||||
.subtitle {
|
||||
color: #c3d6b2;
|
||||
}
|
||||
.card-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
async function sha256(str: string) {
|
||||
const buf = new TextEncoder().encode(str);
|
||||
const hash = await crypto.subtle.digest("SHA-256", buf);
|
||||
return Array.from(new Uint8Array(hash))
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
}
|
||||
|
||||
async function findNonce(challenge: string, difficultyPrefix = "00000") {
|
||||
let nonce = 0;
|
||||
const startTime = performance.now();
|
||||
let lastUpdateTime = startTime;
|
||||
|
||||
while (true) {
|
||||
const hash = await sha256(challenge + nonce);
|
||||
if (hash.startsWith(difficultyPrefix)) {
|
||||
return nonce.toString();
|
||||
}
|
||||
nonce++;
|
||||
|
||||
const currentTime = performance.now();
|
||||
if (currentTime - lastUpdateTime >= 1000) {
|
||||
const elapsedTime = (currentTime - startTime) / 1000;
|
||||
const attemptsPerSecond = Math.floor(nonce / elapsedTime);
|
||||
const estimatedTotalAttempts = Math.pow(16, difficultyPrefix.length);
|
||||
const progress = Math.min(
|
||||
(nonce / estimatedTotalAttempts) * 100,
|
||||
100,
|
||||
).toFixed(2);
|
||||
const eta = (
|
||||
(estimatedTotalAttempts - nonce) /
|
||||
attemptsPerSecond
|
||||
).toFixed(2);
|
||||
|
||||
document.querySelector("#email").innerText =
|
||||
`~${progress}% done, ${attemptsPerSecond}/s, ETA: ${eta}s`;
|
||||
|
||||
lastUpdateTime = currentTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function decryptEmail(e) {
|
||||
e.preventDefault();
|
||||
const { ciphertext, iv, challenge, difficulty } = window.emailData;
|
||||
|
||||
const nonce = await findNonce(challenge, difficulty);
|
||||
const keyMaterial = await sha256(challenge + nonce);
|
||||
const keyBytes = new Uint8Array(
|
||||
keyMaterial.match(/.{1,2}/g).map((b) => parseInt(b, 16)),
|
||||
);
|
||||
const cryptoKey = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
keyBytes,
|
||||
{ name: "AES-GCM" },
|
||||
false,
|
||||
["decrypt"],
|
||||
);
|
||||
const cipherBytes = Uint8Array.from(atob(ciphertext), (c) =>
|
||||
c.charCodeAt(0),
|
||||
);
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{
|
||||
name: "AES-GCM",
|
||||
iv: Uint8Array.from(atob(iv), (c) => c.charCodeAt(0)),
|
||||
},
|
||||
cryptoKey,
|
||||
cipherBytes,
|
||||
);
|
||||
document.querySelector("#email").innerHTML = "";
|
||||
document.querySelector("#email").innerText = new TextDecoder().decode(
|
||||
decrypted,
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue