Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ TrueVote runs entirely in the browser and requires zero server configuration, ma
- When a vote is cast, the nullifier receipt is permanently recorded in browser storage and IndexedDB.
- If the voter returns to the voting page, TrueVote detects the nullifier and replaces candidate selection with an immutable "Ballot Already Cast" confirmation state.

### 3. Real Anti-Bot Heuristics & Cloudflare Turnstile
### 3. Real Anti-Bot Heuristics, Incognito Detection & Cloudflare Turnstile
- Incognito & Private Mode Detection: Automatically detects private browsing across Chromium, Safari, Firefox, and Edge via memory/storage heuristics. If detected, voting is blocked to prevent Sybil attacks and session isolation tampering.
- Cloudflare Turnstile Widget: Embedded proof-of-humanity challenge verifies legitimate traffic.
- Automated Test Runner Detection: Checks `navigator.webdriver` to immediately block headless browser drivers (Selenium, Puppeteer, Playwright).
- Human Entropy Verification: Requires genuine cursor movement (`mousemove`) or mobile touch taps (`touchstart`) before the challenge unlocks, blocking programmatic click scripts.
Expand Down Expand Up @@ -226,6 +227,7 @@ The production bundle will be generated in the `build/` folder.
| Walletless Voting Mode | Fully Operational |
| Client-Side SHA-256 Nullifiers | Fully Operational |
| Revote Prevention & Session Locking | Fully Operational |
| Incognito & Unsafe Browser Guard | Fully Operational |
| Cloudflare Turnstile Anti-Bot | Fully Operational |
| Headless Webdriver Bot Detection | Fully Operational |
| Human Cursor/Touch Entropy Checks | Fully Operational |
Expand Down
Binary file added public/images/istockphoto-1018127028-612x612.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
274 changes: 274 additions & 0 deletions security/detectenv.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
export async function detectIncognito() {
if (typeof window === 'undefined') {
return { isPrivate: false, browserName: 'Server' };
}

return new Promise((resolve) => {
let browserName = 'Unknown';
let settled = false;

function finish(isPrivate, name = browserName) {
if (settled) return;
settled = true;
resolve({ isPrivate: Boolean(isPrivate), browserName: name });
}

setTimeout(() => {
finish(false);
}, 1200);

const ua = navigator.userAgent || '';

function getEngineID() {
try {
const neg = parseInt('-1', 10);
neg.toFixed(neg);
} catch (e) {
return e.message ? e.message.length : 0;
}
return 0;
}

const engineId = getEngineID();
const isSafari = engineId === 44 || engineId === 43;
const isChrome = engineId === 51 || Boolean(window.chrome);
const isFirefox = engineId === 25 || typeof InstallTrigger !== 'undefined';

function identifyChromium() {
if (navigator.brave !== undefined) return 'Brave';
if (ua.includes('Edg/')) return 'Edge';
if (ua.includes('OPR/')) return 'Opera';
if (ua.includes('Chrome/')) return 'Chrome';
return 'Chromium';
}

if (isSafari || (ua.includes('Safari') && !ua.includes('Chrome'))) {
browserName = 'Safari';
try {
if (navigator.storage && typeof navigator.storage.getDirectory === 'function') {
navigator.storage.getDirectory()
.then(() => finish(false))
.catch((e) => {
const msg = (e && e.message) ? String(e.message) : String(e);
finish(msg.includes('unknown transient reason') || msg.includes('Security'));
});
return;
}

const testDbName = '__safari_priv_' + Math.random().toString(36).substring(2);
const req = window.indexedDB.open(testDbName, 1);
req.onupgradeneeded = (ev) => {
try {
const db = ev.target.result;
db.createObjectStore('t', { autoIncrement: true }).put(new Blob());
finish(false);
} catch (err) {
const msg = String(err && err.message ? err.message : err);
finish(msg.includes('are not yet supported') || msg.includes('QuotaExceeded'));
} finally {
try {
ev.target.result.close();
window.indexedDB.deleteDatabase(testDbName);
} catch (ignore) {}
}
};
req.onerror = () => finish(false);
req.onsuccess = () => {
try {
req.result.close();
window.indexedDB.deleteDatabase(testDbName);
} catch (ignore) {}
finish(false);
};
return;
} catch (e) {
finish(false);
return;
}
}

if (isFirefox || ua.includes('Firefox')) {
browserName = 'Firefox';
try {
if (navigator.storage && typeof navigator.storage.getDirectory === 'function') {
navigator.storage.getDirectory()
.then(() => finish(false))
.catch((e) => {
const msg = (e && e.message) ? String(e.message) : String(e);
finish(msg.includes('Security error') || msg.includes('Security'));
});
return;
}

const req = window.indexedDB.open('__ff_private_test');
req.onerror = (e) => {
if (req.error && req.error.name === 'InvalidStateError') {
if (e && e.preventDefault) e.preventDefault();
finish(true);
return;
}
finish(false);
};
req.onsuccess = () => {
try {
req.result.close();
window.indexedDB.deleteDatabase('__ff_private_test');
} catch (ignore) {}
finish(false);
};
return;
} catch (e) {
finish(false);
return;
}
}

if (isChrome || ua.includes('Chrome') || ua.includes('Chromium') || ua.includes('CriOS')) {
browserName = identifyChromium();

if (navigator.storage && navigator.storage.estimate) {
navigator.storage.estimate().then((estimate) => {
if (estimate && typeof estimate.quota === 'number') {
if (estimate.quota < 120000000) {
finish(true);
return;
}
}
runChromiumTiming();
}).catch(() => {
runChromiumTiming();
});
return;
}

runChromiumTiming();
return;
}

function runChromiumTiming() {
try {
if (window.indexedDB) {
const dbName = '__cr_priv_' + Math.random().toString(36).substring(2);
const req = window.indexedDB.open(dbName, 1);
req.onupgradeneeded = () => {
req.result.createObjectStore('s');
};
req.onerror = () => {
try { window.indexedDB.deleteDatabase(dbName); } catch (ignore) {}
finish(false);
};
req.onsuccess = () => {
const db = req.result;
let honored = false;
try {
const t = db.transaction('s', 'readwrite', { durability: 'strict' });
honored = t.durability === 'strict';
t.abort();
} catch (ignore) {}

if (!honored) {
db.close();
try { window.indexedDB.deleteDatabase(dbName); } catch (ignore) {}
finish(false);
return;
}

const payload = new Uint8Array(16384);
const block = (durability) => new Promise((res, rej) => {
const t0 = performance.now();
let i = 0;
const step = () => {
if (i === 12) { res(performance.now() - t0); return; }
const tx = db.transaction('s', 'readwrite', { durability });
tx.objectStore('s').put(payload, i);
i++;
tx.oncomplete = step;
tx.onerror = tx.onabort = () => rej(tx.error);
};
step();
});

(async () => {
try {
await block('relaxed');
await block('strict');
const ratios = [];
for (let r = 0; r < 8; r++) {
const rel = await block('relaxed');
const str = await block('strict');
ratios.push(rel > 0 ? str / rel : 1.0);
}
ratios.sort((a, b) => a - b);
db.close();
try { window.indexedDB.deleteDatabase(dbName); } catch (ignore) {}
const medianRatio = ratios[Math.floor(ratios.length / 2)];
finish(medianRatio < 1.3);
} catch (err) {
db.close();
try { window.indexedDB.deleteDatabase(dbName); } catch (ignore) {}
finish(false);
}
})();
};
return;
}
} catch (err) {
finish(false);
return;
}
finish(false);
}

finish(false);
});
}

export async function detectSafeEnvironment() {
if (typeof window === 'undefined') {
return { isSafe: true, isIncognito: false, isAutomated: false, reason: '' };
}

if (navigator.webdriver) {
return {
isSafe: false,
isIncognito: false,
isAutomated: true,
reason: 'Automated headless browser runner detected (navigator.webdriver).',
};
}

try {
const incognitoResult = await detectIncognito();
if (incognitoResult.isPrivate) {
return {
isSafe: false,
isIncognito: true,
isAutomated: false,
reason: 'Private/Incognito browsing mode detected.',
browserName: incognitoResult.browserName,
};
}
} catch (err) {}

try {
const testKey = '__tv_env_test__';
window.localStorage.setItem(testKey, '1');
window.localStorage.removeItem(testKey);
} catch (e) {
return {
isSafe: false,
isIncognito: true,
isAutomated: false,
reason: 'Browser storage access is blocked or restricted.',
};
}

return {
isSafe: true,
isIncognito: false,
isAutomated: false,
reason: '',
};
}

export default detectSafeEnvironment;
21 changes: 20 additions & 1 deletion src/Voting.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { render, screen, waitFor } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import VotingPage from './voting/VotingPage';

Expand All @@ -24,5 +24,24 @@ describe('VotingPage Component', () => {
const castBtn = screen.getByRole('button', { name: /Cast Anonymous Vote/i });
expect(castBtn).toBeDisabled();
});

test('blocks voting and shows unsafe browser illustration when incognito is flagged', async () => {
const detectEnv = require('./security/detectenv');
const spy = jest.spyOn(detectEnv, 'detectSafeEnvironment').mockResolvedValue({
isSafe: false,
isIncognito: true,
isAutomated: false,
reason: 'Private/Incognito browsing mode detected.',
});

renderWithRouter(<VotingPage />);

await waitFor(() => {
expect(screen.getByText(/not safe browser u cant vote here/i)).toBeInTheDocument();
expect(screen.getByAltText(/not safe browser u cant vote here/i)).toBeInTheDocument();
});

spy.mockRestore();
});
});

Loading
Loading