Added POW before viewing contact info

This commit is contained in:
theamazing0 2025-07-26 20:10:49 -04:00
parent 6f303a0f03
commit 2cce03ecf3
4 changed files with 248 additions and 175 deletions

View file

@ -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 -&gt;</a>
<a data-link>Open -&gt;</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;
}
}

View 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>

View file

@ -16,7 +16,7 @@
@media only screen and (min-width: 769px) {
justify-content: unset;
border-radius: 8px;
border-radius: 10px;
width: fit-content;
}
}

View file

@ -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>