fix(app): passphrase dialog — remove CSP meta, check crypto.subtle availability
- Remove CSP meta tag from index.html (breaks Vite HMR, should be set via HTTP headers in production nginx instead) - isCryptoEnabled() now checks crypto.subtle is available (undefined over HTTP on non-localhost origins) - Add try/catch + error feedback to passphrase submit flow - PassphraseDialog accepts error prop, focuses input on visible Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
3620b1f4d5
commit
c84c0fb424
@@ -10,7 +10,7 @@
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="AIUI" />
|
||||
<meta name="description" content="AI chat interface with rich content surfaces" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://image.tmdb.org https://covers.openlibrary.org https://is1-ssl.mzstatic.com https://upload.wikimedia.org https://books.google.com https://*.googleusercontent.com; font-src 'self'; connect-src 'self' https://mempool.space https://itunes.apple.com https://openlibrary.org https://www.googleapis.com https://en.wikipedia.org https://openrouter.ai wss:; media-src 'self' blob: https:; frame-src https://www.youtube-nocookie.com https://*.odysee.com; object-src 'none'; base-uri 'self';" />
|
||||
<!-- CSP set via HTTP headers in production nginx — not in HTML meta to avoid breaking Vite HMR -->
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon-180x180.png" />
|
||||
<title>AIUI</title>
|
||||
|
||||
+21
-12
@@ -6,6 +6,7 @@
|
||||
<PassphraseDialog
|
||||
:visible="showPassphrase"
|
||||
:is-creating="isCreatingPassphrase"
|
||||
:error="passphraseError"
|
||||
@submit="handlePassphraseSubmit"
|
||||
@skip="showPassphrase = false"
|
||||
/>
|
||||
@@ -50,21 +51,29 @@ const isCreatingPassphrase = ref(false)
|
||||
|
||||
const SALT_KEY = 'aiui-crypto-salt'
|
||||
|
||||
const passphraseError = ref('')
|
||||
|
||||
async function handlePassphraseSubmit(passphrase: string) {
|
||||
let saltHex = localStorage.getItem(SALT_KEY)
|
||||
let salt: Uint8Array
|
||||
passphraseError.value = ''
|
||||
try {
|
||||
let saltHex = localStorage.getItem(SALT_KEY)
|
||||
let salt: Uint8Array
|
||||
|
||||
if (saltHex) {
|
||||
salt = new Uint8Array(saltHex.match(/.{2}/g)!.map(b => parseInt(b, 16)))
|
||||
} else {
|
||||
salt = await generateSalt()
|
||||
saltHex = Array.from(salt).map(b => b.toString(16).padStart(2, '0')).join('')
|
||||
localStorage.setItem(SALT_KEY, saltHex)
|
||||
if (saltHex) {
|
||||
salt = new Uint8Array(saltHex.match(/.{2}/g)!.map(b => parseInt(b, 16)))
|
||||
} else {
|
||||
salt = await generateSalt()
|
||||
saltHex = Array.from(salt).map(b => b.toString(16).padStart(2, '0')).join('')
|
||||
localStorage.setItem(SALT_KEY, saltHex)
|
||||
}
|
||||
|
||||
const key = await deriveKey(passphrase, salt)
|
||||
setSessionKey(key, salt)
|
||||
showPassphrase.value = false
|
||||
} catch (err) {
|
||||
console.error('[AIUI] Passphrase error:', err)
|
||||
passphraseError.value = 'Encryption failed — check your passphrase or try skipping'
|
||||
}
|
||||
|
||||
const key = await deriveKey(passphrase, salt)
|
||||
setSessionKey(key, salt)
|
||||
showPassphrase.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -54,8 +54,8 @@
|
||||
@keydown.enter="handleSubmit"
|
||||
/>
|
||||
|
||||
<p v-if="errorMessage" class="text-xs text-red-400 px-1">
|
||||
{{ errorMessage }}
|
||||
<p v-if="errorMessage || props.error" class="text-xs text-red-400 px-1">
|
||||
{{ errorMessage || props.error }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -86,11 +86,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick } from 'vue'
|
||||
import { ref, computed, watch, nextTick } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
isCreating: boolean
|
||||
error?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -124,7 +125,7 @@ function handleSkip() {
|
||||
emit('skip')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => inputRef.value?.focus())
|
||||
})
|
||||
watch(() => props.visible, (v) => {
|
||||
if (v) nextTick(() => inputRef.value?.focus())
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
@@ -105,13 +105,17 @@ function base64ToBuffer(base64: string): Uint8Array {
|
||||
|
||||
/**
|
||||
* Check if encryption should be enabled.
|
||||
* Disabled in dev mode with VITE_DISABLE_CRYPTO=true.
|
||||
* Disabled in dev mode with VITE_DISABLE_CRYPTO=true, or when
|
||||
* Web Crypto API is unavailable (HTTP on non-localhost origins).
|
||||
*/
|
||||
export function isCryptoEnabled(): boolean {
|
||||
try {
|
||||
return import.meta.env.VITE_DISABLE_CRYPTO !== 'true'
|
||||
} catch {
|
||||
if (import.meta.env.VITE_DISABLE_CRYPTO === 'true') return false
|
||||
// crypto.subtle is only available in secure contexts (HTTPS or localhost)
|
||||
if (typeof globalThis.crypto?.subtle === 'undefined') return false
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user