fix: close APT adversarial runtime gaps
This commit is contained in:
@@ -1,8 +1,26 @@
|
||||
'use strict';
|
||||
|
||||
const APT_URL = 'http://127.0.0.1:18247/cookie';
|
||||
const CHALLENGE_URL = 'http://127.0.0.1:18247/challenge';
|
||||
const PAIRING_STORAGE_KEY = 'aptPairingSecret';
|
||||
const PAIRING_SECRET_PATTERN = /^[A-Za-z0-9_-]{43}$/;
|
||||
const NONCE_PATTERN = PAIRING_SECRET_PATTERN;
|
||||
const BRIDGE_DEADLINE_MS = 3000;
|
||||
|
||||
function base64Url(bytes) {
|
||||
let binary = '';
|
||||
for (const byte of bytes) binary += String.fromCharCode(byte);
|
||||
return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
|
||||
}
|
||||
|
||||
async function hmacBase64Url(cryptoApi, secret, value) {
|
||||
const encoder = new TextEncoder();
|
||||
const key = await cryptoApi.subtle.importKey(
|
||||
'raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']
|
||||
);
|
||||
const signature = await cryptoApi.subtle.sign('HMAC', key, encoder.encode(value));
|
||||
return base64Url(new Uint8Array(signature));
|
||||
}
|
||||
|
||||
function isSupportedDeploymentUrl(value) {
|
||||
try {
|
||||
@@ -20,7 +38,8 @@ function createPopupController({
|
||||
documentApi,
|
||||
navigatorApi,
|
||||
fetchImpl,
|
||||
confirmCopy
|
||||
confirmCopy,
|
||||
cryptoApi
|
||||
}) {
|
||||
const tabInfo = documentApi.getElementById('tabInfo');
|
||||
const pairingInfo = documentApi.getElementById('pairingInfo');
|
||||
@@ -34,6 +53,16 @@ function createPopupController({
|
||||
let pairingSecret = null;
|
||||
let busy = false;
|
||||
|
||||
async function bridgeFetch(url, options) {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), BRIDGE_DEADLINE_MS);
|
||||
try {
|
||||
return await fetchImpl(url, { ...options, signal: controller.signal });
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
function showStatus(message, type) {
|
||||
statusMsg.textContent = message;
|
||||
statusMsg.className = `status-msg ${type}`;
|
||||
@@ -77,15 +106,45 @@ function createPopupController({
|
||||
setBusy(true);
|
||||
let cookieValue = null;
|
||||
try {
|
||||
showStatus('Authenticating Alta Proxy Tool...', 'info');
|
||||
if (!cryptoApi || !cryptoApi.subtle || typeof cryptoApi.getRandomValues !== 'function') {
|
||||
throw new Error('CRYPTO_UNAVAILABLE');
|
||||
}
|
||||
const clientNonce = base64Url(cryptoApi.getRandomValues(new Uint8Array(32)));
|
||||
const challengeResponse = await bridgeFetch(CHALLENGE_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ clientNonce })
|
||||
});
|
||||
const challenge = await challengeResponse.json();
|
||||
if (!challengeResponse.ok || !challenge || challenge.success !== true ||
|
||||
!NONCE_PATTERN.test(challenge.serverNonce) || !NONCE_PATTERN.test(challenge.serverProof)) {
|
||||
throw new Error('BRIDGE_REJECTED');
|
||||
}
|
||||
const expectedServerProof = await hmacBase64Url(
|
||||
cryptoApi,
|
||||
pairingSecret,
|
||||
JSON.stringify(['apt-server-challenge-v1', clientNonce, challenge.serverNonce])
|
||||
);
|
||||
if (challenge.serverProof !== expectedServerProof) throw new Error('BRIDGE_AUTH_FAILED');
|
||||
|
||||
showStatus('Sending to Alta Proxy Tool...', 'info');
|
||||
cookieValue = await getVaCookieValue();
|
||||
const response = await fetchImpl(APT_URL, {
|
||||
const cookieRequest = {
|
||||
clientNonce,
|
||||
serverNonce: challenge.serverNonce,
|
||||
deploymentUrl: detectedOrigin,
|
||||
cookieValue
|
||||
};
|
||||
cookieRequest.proof = await hmacBase64Url(
|
||||
cryptoApi,
|
||||
pairingSecret,
|
||||
JSON.stringify(['apt-cookie-request-v1', clientNonce, challenge.serverNonce, detectedOrigin, cookieValue])
|
||||
);
|
||||
const response = await bridgeFetch(APT_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-APT-Pairing': pairingSecret
|
||||
},
|
||||
body: JSON.stringify({ deploymentUrl: detectedOrigin, cookieValue })
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(cookieRequest)
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!response.ok || !data || data.success !== true) throw new Error('BRIDGE_REJECTED');
|
||||
@@ -173,7 +232,8 @@ if (typeof module !== 'undefined' && module.exports) {
|
||||
documentApi: document,
|
||||
navigatorApi: navigator,
|
||||
fetchImpl: fetch,
|
||||
confirmCopy: (message) => window.confirm(message)
|
||||
confirmCopy: (message) => window.confirm(message),
|
||||
cryptoApi: crypto
|
||||
}).init().catch(() => {
|
||||
const status = document.getElementById('statusMsg');
|
||||
status.textContent = 'Extension initialization failed. Open pairing settings and try again.';
|
||||
|
||||
Reference in New Issue
Block a user