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:
Dorian
2026-03-05 06:58:55 +00:00
co-authored by Claude Opus 4.6
parent 3620b1f4d5
commit c84c0fb424
4 changed files with 36 additions and 22 deletions
+1 -1
View File
@@ -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
View File
@@ -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>
+7 -3
View File
@@ -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
}
}