nu
This commit is contained in:
parent
e8864cc853
commit
1d42a9a623
9 changed files with 2122 additions and 542 deletions
|
|
@ -9,6 +9,8 @@
|
|||
let username = '';
|
||||
let password = '';
|
||||
let confirmPassword = '';
|
||||
let keyPassphrase = '';
|
||||
let confirmKeyPassphrase = '';
|
||||
let error = '';
|
||||
let loading = false;
|
||||
let pgpLoading = false;
|
||||
|
|
@ -22,10 +24,14 @@
|
|||
let showGeneratedKeys = false;
|
||||
let generatedPrivateKey = '';
|
||||
let generatedPublicKey = '';
|
||||
let saveKeyLocally = false;
|
||||
|
||||
// Show PGP command example
|
||||
let showPgpExample = false;
|
||||
|
||||
// For unlocking stored key
|
||||
let storedKeyPassphrase = '';
|
||||
|
||||
onMount(async () => {
|
||||
await auth.init();
|
||||
if ($isAuthenticated) {
|
||||
|
|
@ -82,11 +88,22 @@
|
|||
return;
|
||||
}
|
||||
|
||||
if (keyPassphrase !== confirmKeyPassphrase) {
|
||||
error = 'Key passphrases do not match';
|
||||
return;
|
||||
}
|
||||
|
||||
const passphraseError = pgp.validatePassphrase(keyPassphrase);
|
||||
if (passphraseError) {
|
||||
error = passphraseError;
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
// Generate PGP key pair
|
||||
const keyPair = await pgp.generateKeyPair(username);
|
||||
// Generate PGP key pair with passphrase
|
||||
const keyPair = await pgp.generateKeyPair(username, keyPassphrase);
|
||||
|
||||
const result = await auth.register({
|
||||
username,
|
||||
|
|
@ -96,10 +113,7 @@
|
|||
});
|
||||
|
||||
if (result.success) {
|
||||
// Store private key locally
|
||||
pgp.storePrivateKey(keyPair.privateKey);
|
||||
|
||||
// Show keys
|
||||
// Save keys for display
|
||||
generatedPrivateKey = keyPair.privateKey;
|
||||
generatedPublicKey = keyPair.publicKey;
|
||||
showGeneratedKeys = true;
|
||||
|
|
@ -107,13 +121,38 @@
|
|||
error = result.error;
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'Failed to generate PGP keys';
|
||||
error = e.message || 'Failed to generate PGP keys';
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
loading = false;
|
||||
}
|
||||
|
||||
async function proceedAfterKeys() {
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
// If user wants to save locally, store encrypted
|
||||
if (saveKeyLocally) {
|
||||
await pgp.saveEncryptedPrivateKey(keyPassphrase, generatedPrivateKey);
|
||||
}
|
||||
|
||||
// Auto-login after registration
|
||||
const result = await auth.login({ username, password });
|
||||
if (result.success) {
|
||||
goto('/');
|
||||
} else {
|
||||
showGeneratedKeys = false;
|
||||
mode = 'login';
|
||||
error = 'Registration successful. Please login.';
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'Failed to save keys: ' + e.message;
|
||||
}
|
||||
|
||||
loading = false;
|
||||
}
|
||||
|
||||
async function initiatePgpLogin() {
|
||||
error = '';
|
||||
pgpLoading = true;
|
||||
|
|
@ -133,6 +172,13 @@
|
|||
|
||||
// Clear the signature field
|
||||
pgpSignature = '';
|
||||
|
||||
// Check if we have a stored key
|
||||
const hasKey = await pgp.hasEncryptedKey();
|
||||
if (hasKey) {
|
||||
// We'll need to unlock it
|
||||
storedKeyPassphrase = '';
|
||||
}
|
||||
} else {
|
||||
error = data.error || 'User not found or PGP not enabled';
|
||||
}
|
||||
|
|
@ -144,6 +190,26 @@
|
|||
pgpLoading = false;
|
||||
}
|
||||
|
||||
async function signWithStoredKey() {
|
||||
error = '';
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
// Unlock the stored private key
|
||||
const privateKey = await pgp.unlockPrivateKey(storedKeyPassphrase);
|
||||
|
||||
// Sign the challenge
|
||||
pgpSignature = await pgp.signMessage(pgpChallenge, privateKey, storedKeyPassphrase);
|
||||
|
||||
// Clear passphrase for security
|
||||
storedKeyPassphrase = '';
|
||||
} catch (e) {
|
||||
error = 'Failed to unlock key or sign: ' + e.message;
|
||||
}
|
||||
|
||||
loading = false;
|
||||
}
|
||||
|
||||
async function handlePgpLogin() {
|
||||
error = '';
|
||||
loading = true;
|
||||
|
|
@ -172,6 +238,7 @@
|
|||
pgpChallenge = '';
|
||||
pgpPublicKey = '';
|
||||
pgpSignature = '';
|
||||
storedKeyPassphrase = '';
|
||||
error = '';
|
||||
}
|
||||
|
||||
|
|
@ -189,27 +256,13 @@
|
|||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
async function proceedAfterKeys() {
|
||||
// Auto-login after registration
|
||||
loading = true;
|
||||
const result = await auth.login({ username, password });
|
||||
if (result.success) {
|
||||
goto('/');
|
||||
} else {
|
||||
showGeneratedKeys = false;
|
||||
mode = 'login';
|
||||
error = 'Registration successful. Please login.';
|
||||
}
|
||||
loading = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="auth-container">
|
||||
{#if showGeneratedKeys}
|
||||
<h1>Your PGP Keys</h1>
|
||||
<p style="color: var(--error); margin-bottom: 1rem;">
|
||||
<strong>Important:</strong> Save your private key securely. You will need it to login with PGP.
|
||||
<strong>Important:</strong> Save your private key securely. You will need it and your passphrase to login with PGP.
|
||||
</p>
|
||||
|
||||
<div class="form-group">
|
||||
|
|
@ -231,7 +284,7 @@
|
|||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Private Key</label>
|
||||
<label>Private Key (Encrypted with your passphrase)</label>
|
||||
<textarea
|
||||
readonly
|
||||
rows="10"
|
||||
|
|
@ -243,13 +296,27 @@
|
|||
Copy
|
||||
</button>
|
||||
<button type="button" class="btn-secondary" on:click={() => downloadKey(generatedPrivateKey, `${username}-private-key.asc`)}>
|
||||
Download
|
||||
Download (REQUIRED!)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="background: rgba(255, 193, 7, 0.1); border: 1px solid rgba(255, 193, 7, 0.3); padding: 1rem; border-radius: 4px;">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; margin: 0;">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={saveKeyLocally}
|
||||
style="width: 20px; height: 20px;"
|
||||
/>
|
||||
Save encrypted key in this browser for easier PGP login
|
||||
</label>
|
||||
<small style="color: var(--gray); display: block; margin-top: 0.5rem;">
|
||||
The key will be encrypted with your passphrase and stored locally. You can export or remove it later from Settings.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<button class="btn-block" on:click={proceedAfterKeys} disabled={loading}>
|
||||
{loading ? 'Logging in...' : 'Continue'}
|
||||
{loading ? 'Saving...' : 'Continue'}
|
||||
</button>
|
||||
{:else}
|
||||
<h1>{mode === 'login' ? 'Login' : 'Register'}</h1>
|
||||
|
|
@ -303,6 +370,37 @@
|
|||
<form on:submit|preventDefault={handlePgpLogin}>
|
||||
<h3 style="margin-bottom: 1rem;">PGP Authentication</h3>
|
||||
|
||||
{#await pgp.hasEncryptedKey() then hasKey}
|
||||
{#if hasKey && !pgpSignature}
|
||||
<div class="instruction-box" style="background: rgba(40, 167, 69, 0.1); border-color: rgba(40, 167, 69, 0.3);">
|
||||
<p><strong>Stored key detected!</strong></p>
|
||||
<p>Enter your passphrase to unlock your private key and sign automatically:</p>
|
||||
|
||||
<div class="form-group" style="margin: 1rem 0;">
|
||||
<input
|
||||
type="password"
|
||||
bind:value={storedKeyPassphrase}
|
||||
placeholder="Enter your key passphrase"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
on:click={signWithStoredKey}
|
||||
disabled={loading || !storedKeyPassphrase}
|
||||
style="margin-bottom: 0.5rem;"
|
||||
>
|
||||
{loading ? 'Signing...' : 'Unlock & Sign'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style="margin: 1rem 0; text-align: center; color: var(--gray);">
|
||||
OR sign manually
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
|
||||
<div class="instruction-box">
|
||||
<p><strong>Step 1:</strong> Copy this challenge text:</p>
|
||||
<div class="pgp-key" style="margin: 0.5rem 0;">
|
||||
|
|
@ -319,7 +417,7 @@
|
|||
|
||||
<p><strong>Step 2:</strong> Sign it with your private key using GPG or another PGP tool</p>
|
||||
<p style="color: var(--gray); font-size: 0.85rem; margin-top: 0.5rem;">
|
||||
Note: For security, we never handle your private keys. All signing must be done on your device.
|
||||
Note: Your private key passphrase is required to unlock your key for signing.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -339,7 +437,7 @@
|
|||
# Save the challenge to a file
|
||||
echo "{pgpChallenge}" > challenge.txt
|
||||
|
||||
# Sign with your private key
|
||||
# Sign with your private key (will prompt for passphrase)
|
||||
gpg --armor --detach-sign challenge.txt
|
||||
|
||||
# This creates challenge.txt.asc with the signature
|
||||
|
|
@ -350,16 +448,9 @@ gpg --armor --detach-sign challenge.txt
|
|||
<li>Save the challenge text to a file</li>
|
||||
<li>Right-click the file → Sign</li>
|
||||
<li>Select your key and choose "Create detached signature"</li>
|
||||
<li>Enter your passphrase when prompted</li>
|
||||
<li>Open the .asc file and copy its contents</li>
|
||||
</ol>
|
||||
|
||||
<h4>Using GPG Suite (Mac):</h4>
|
||||
<ol style="font-size: 0.9rem; margin: 0.5rem 0;">
|
||||
<li>Save the challenge text to a file</li>
|
||||
<li>Right-click the file → Services → OpenPGP: Sign File</li>
|
||||
<li>Choose "Create Detached Signature"</li>
|
||||
<li>Open the .sig file and copy its contents</li>
|
||||
</ol>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
|
@ -442,6 +533,40 @@ iQEcBAABCAAGBQJe...
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div style="background: rgba(86, 29, 94, 0.1); border: 1px solid rgba(86, 29, 94, 0.3); padding: 1rem; border-radius: 8px; margin: 1.5rem 0;">
|
||||
<h3 style="margin: 0 0 1rem 0; font-size: 1.1rem; color: var(--primary);">🔐 PGP Key Passphrase</h3>
|
||||
<p style="font-size: 0.9rem; color: var(--gray); margin-bottom: 1rem;">
|
||||
This passphrase encrypts your PGP private key. You'll need it every time you use PGP login.
|
||||
</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="key-passphrase">Key Passphrase</label>
|
||||
<input
|
||||
type="password"
|
||||
id="key-passphrase"
|
||||
bind:value={keyPassphrase}
|
||||
required
|
||||
disabled={loading}
|
||||
placeholder="Enter a strong passphrase (12+ characters)"
|
||||
/>
|
||||
<small style="color: var(--gray);">
|
||||
Must be 12+ characters with 3 of: uppercase, lowercase, numbers, special characters
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="confirm-key-passphrase">Confirm Key Passphrase</label>
|
||||
<input
|
||||
type="password"
|
||||
id="confirm-key-passphrase"
|
||||
bind:value={confirmKeyPassphrase}
|
||||
required
|
||||
disabled={loading}
|
||||
placeholder="Confirm your key passphrase"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="error">{error}</div>
|
||||
{/if}
|
||||
|
|
@ -451,18 +576,18 @@ iQEcBAABCAAGBQJe...
|
|||
</button>
|
||||
|
||||
<p style="margin-top: 1rem; font-size: 0.9rem; color: var(--gray);">
|
||||
A PGP key pair will be generated for your account. You'll be able to save both keys after registration.
|
||||
A PGP key pair will be generated and encrypted with your passphrase.
|
||||
</p>
|
||||
</form>
|
||||
{/if}
|
||||
|
||||
<div style="margin-top: 2rem; text-align: center;">
|
||||
{#if mode === 'login'}
|
||||
<button class="btn-link" on:click={() => { mode = 'register'; error = ''; }}>
|
||||
<button class="btn-link" on:click={() => { mode = 'register'; error = ''; username = ''; password = ''; }}>
|
||||
Need an account? Register
|
||||
</button>
|
||||
{:else}
|
||||
<button class="btn-link" on:click={() => { mode = 'login'; error = ''; }}>
|
||||
<button class="btn-link" on:click={() => { mode = 'login'; error = ''; username = ''; password = ''; keyPassphrase = ''; confirmKeyPassphrase = ''; }}>
|
||||
Already have an account? Login
|
||||
</button>
|
||||
{/if}
|
||||
|
|
@ -531,4 +656,14 @@ iQEcBAABCAAGBQJe...
|
|||
.example-section ol li {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.pgp-key {
|
||||
font-family: monospace;
|
||||
font-size: 0.8rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -54,6 +54,14 @@
|
|||
let showGeneratedKeys = false;
|
||||
let generatedPrivateKey = '';
|
||||
let generatedPublicKey = '';
|
||||
let keyPassphrase = '';
|
||||
let confirmKeyPassphrase = '';
|
||||
let saveKeyLocally = false;
|
||||
let hasEncryptedKey = false;
|
||||
|
||||
// Unlock key dialog
|
||||
let showUnlockDialog = false;
|
||||
let unlockPassphrase = '';
|
||||
|
||||
// User data for safe access
|
||||
let currentUser = null;
|
||||
|
|
@ -71,10 +79,13 @@
|
|||
// Store user for safe access
|
||||
currentUser = $auth.user;
|
||||
|
||||
// Check if we have an encrypted key stored
|
||||
hasEncryptedKey = await pgp.hasEncryptedKey();
|
||||
|
||||
// Get fresh user data to ensure we have latest values
|
||||
try {
|
||||
const response = await fetch('/api/user/me', {
|
||||
headers: { 'Authorization': `Bearer ${$auth.token}` }
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
|
|
@ -111,11 +122,11 @@
|
|||
});
|
||||
|
||||
async function loadPgpKeys() {
|
||||
if (!browser || !$auth.token) return;
|
||||
if (!browser) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/user/pgp-keys', {
|
||||
headers: { 'Authorization': `Bearer ${$auth.token}` }
|
||||
credentials: 'include'
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
|
|
@ -148,10 +159,8 @@
|
|||
// Update bio
|
||||
const response = await fetch('/api/user/profile', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${$auth.token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ bio })
|
||||
});
|
||||
|
||||
|
|
@ -170,9 +179,7 @@
|
|||
|
||||
const avatarResponse = await fetch('/api/user/avatar', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${$auth.token}`
|
||||
},
|
||||
credentials: 'include',
|
||||
body: formData
|
||||
});
|
||||
|
||||
|
|
@ -236,10 +243,8 @@
|
|||
try {
|
||||
const response = await fetch('/api/user/password', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${$auth.token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ oldPassword, newPassword })
|
||||
});
|
||||
|
||||
|
|
@ -266,37 +271,37 @@
|
|||
loading = false;
|
||||
}
|
||||
|
||||
async function updateColor() {
|
||||
if (!newColor || newColor === userColor) {
|
||||
colorError = 'Please select a different color';
|
||||
return;
|
||||
}
|
||||
|
||||
colorLoading = true;
|
||||
colorError = '';
|
||||
colorMessage = '';
|
||||
|
||||
const result = await auth.updateColor(newColor);
|
||||
|
||||
if (result.success) {
|
||||
userColor = result.color;
|
||||
colorMessage = 'Color updated successfully!';
|
||||
showColorPicker = false;
|
||||
|
||||
// Refresh the current user data to ensure consistency
|
||||
if (currentUser) {
|
||||
currentUser = { ...currentUser, userColor: result.color, colorCode: result.color };
|
||||
async function updateColor() {
|
||||
if (!newColor || newColor === userColor) {
|
||||
colorError = 'Please select a different color';
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear message after 3 seconds
|
||||
setTimeout(() => { colorMessage = ''; }, 3000);
|
||||
} else {
|
||||
colorError = result.error || 'Failed to update color';
|
||||
colorLoading = true;
|
||||
colorError = '';
|
||||
colorMessage = '';
|
||||
|
||||
const result = await auth.updateColor(newColor);
|
||||
|
||||
if (result.success) {
|
||||
userColor = result.color;
|
||||
colorMessage = 'Color updated successfully!';
|
||||
showColorPicker = false;
|
||||
|
||||
// Refresh the current user data to ensure consistency
|
||||
if (currentUser) {
|
||||
currentUser = { ...currentUser, userColor: result.color, colorCode: result.color };
|
||||
}
|
||||
|
||||
// Clear message after 3 seconds
|
||||
setTimeout(() => { colorMessage = ''; }, 3000);
|
||||
} else {
|
||||
colorError = result.error || 'Failed to update color';
|
||||
}
|
||||
|
||||
colorLoading = false;
|
||||
}
|
||||
|
||||
colorLoading = false;
|
||||
}
|
||||
|
||||
function selectSuggestedColor(color) {
|
||||
newColor = color;
|
||||
}
|
||||
|
|
@ -355,10 +360,8 @@ async function updateColor() {
|
|||
try {
|
||||
const response = await fetch('/api/user/pgp-only', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${$auth.token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ enable: true })
|
||||
});
|
||||
|
||||
|
|
@ -407,19 +410,46 @@ async function updateColor() {
|
|||
async function generateNewKeyPair() {
|
||||
if (!browser) return;
|
||||
|
||||
loading = true;
|
||||
error = '';
|
||||
keyPassphrase = '';
|
||||
confirmKeyPassphrase = '';
|
||||
saveKeyLocally = false;
|
||||
|
||||
// Prompt for passphrase first
|
||||
showGeneratedKeys = true;
|
||||
generatedPrivateKey = '';
|
||||
generatedPublicKey = '';
|
||||
}
|
||||
|
||||
async function generateKeysWithPassphrase() {
|
||||
error = '';
|
||||
|
||||
if (!keyPassphrase) {
|
||||
error = 'Passphrase is required';
|
||||
return;
|
||||
}
|
||||
|
||||
if (keyPassphrase !== confirmKeyPassphrase) {
|
||||
error = 'Passphrases do not match';
|
||||
return;
|
||||
}
|
||||
|
||||
const passphraseError = pgp.validatePassphrase(keyPassphrase);
|
||||
if (passphraseError) {
|
||||
error = passphraseError;
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
const keyPair = await pgp.generateKeyPair(currentUser?.username || 'user');
|
||||
const keyPair = await pgp.generateKeyPair(currentUser?.username || 'user', keyPassphrase);
|
||||
|
||||
// Show keys instead of auto-downloading
|
||||
generatedPrivateKey = keyPair.privateKey;
|
||||
generatedPublicKey = keyPair.publicKey;
|
||||
showGeneratedKeys = true;
|
||||
|
||||
} catch (e) {
|
||||
error = 'Failed to generate key pair';
|
||||
error = 'Failed to generate key pair: ' + e.message;
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
|
|
@ -431,14 +461,13 @@ async function updateColor() {
|
|||
error = '';
|
||||
|
||||
try {
|
||||
// Save public key to server
|
||||
const fingerprint = await pgp.getFingerprint(generatedPublicKey);
|
||||
|
||||
const response = await fetch('/api/user/pgp-key', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${$auth.token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
publicKey: generatedPublicKey,
|
||||
fingerprint: fingerprint
|
||||
|
|
@ -448,8 +477,11 @@ async function updateColor() {
|
|||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.success) {
|
||||
// Store private key locally
|
||||
pgp.storePrivateKey(generatedPrivateKey);
|
||||
// If user wants to save locally, encrypt and store
|
||||
if (saveKeyLocally) {
|
||||
await pgp.saveEncryptedPrivateKey(keyPassphrase, generatedPrivateKey);
|
||||
hasEncryptedKey = true;
|
||||
}
|
||||
|
||||
message = 'New key pair saved successfully';
|
||||
error = '';
|
||||
|
|
@ -457,6 +489,8 @@ async function updateColor() {
|
|||
showGeneratedKeys = false;
|
||||
generatedPrivateKey = '';
|
||||
generatedPublicKey = '';
|
||||
keyPassphrase = '';
|
||||
confirmKeyPassphrase = '';
|
||||
|
||||
// Clear message after 3 seconds
|
||||
setTimeout(() => { message = ''; }, 3000);
|
||||
|
|
@ -487,10 +521,8 @@ async function updateColor() {
|
|||
|
||||
const response = await fetch('/api/user/pgp-key', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${$auth.token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
publicKey: newPublicKey,
|
||||
fingerprint
|
||||
|
|
@ -583,6 +615,35 @@ async function updateColor() {
|
|||
});
|
||||
}
|
||||
|
||||
async function showUnlockKeyDialog() {
|
||||
showUnlockDialog = true;
|
||||
unlockPassphrase = '';
|
||||
}
|
||||
|
||||
async function unlockAndExportKey() {
|
||||
error = '';
|
||||
|
||||
try {
|
||||
const privateKey = await pgp.unlockPrivateKey(unlockPassphrase);
|
||||
downloadKey(privateKey, `${displayUsername}-private-key.asc`);
|
||||
showUnlockDialog = false;
|
||||
unlockPassphrase = '';
|
||||
message = 'Private key exported successfully';
|
||||
setTimeout(() => { message = ''; }, 3000);
|
||||
} catch (e) {
|
||||
error = 'Failed to unlock key: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function removeLocalKey() {
|
||||
if (confirm('Remove the encrypted private key from this browser? You will need to re-import it later to use PGP login.')) {
|
||||
await pgp.removeEncryptedPrivateKey();
|
||||
hasEncryptedKey = false;
|
||||
message = 'Local key removed';
|
||||
setTimeout(() => { message = ''; }, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
// Track profile changes
|
||||
$: if (browser) {
|
||||
hasProfileChanges = bio !== (currentUser?.bio || '') || avatarFile !== null;
|
||||
|
|
@ -910,6 +971,55 @@ async function updateColor() {
|
|||
border-color: var(--white);
|
||||
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.passphrase-info {
|
||||
background: rgba(86, 29, 94, 0.1);
|
||||
border: 1px solid rgba(86, 29, 94, 0.3);
|
||||
border-radius: 4px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.passphrase-info h4 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.passphrase-info ul {
|
||||
margin: 0;
|
||||
padding-left: 1.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--gray);
|
||||
}
|
||||
|
||||
.key-storage-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
background: rgba(255, 193, 7, 0.1);
|
||||
border: 1px solid rgba(255, 193, 7, 0.3);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.local-key-info {
|
||||
background: rgba(40, 167, 69, 0.1);
|
||||
border: 1px solid rgba(40, 167, 69, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.local-key-info p {
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.local-key-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container">
|
||||
|
|
@ -1257,6 +1367,26 @@ async function updateColor() {
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
{#if hasEncryptedKey}
|
||||
<div class="local-key-info">
|
||||
<p>
|
||||
<span style="color: var(--success);">🔑</span>
|
||||
<strong>Encrypted private key stored in this browser</strong>
|
||||
</p>
|
||||
<p style="font-size: 0.9rem; color: var(--gray);">
|
||||
Your private key is encrypted and stored locally. You'll need your passphrase to use it.
|
||||
</p>
|
||||
<div class="local-key-actions">
|
||||
<button on:click={showUnlockKeyDialog}>
|
||||
Export Private Key
|
||||
</button>
|
||||
<button class="btn btn-danger" on:click={removeLocalKey}>
|
||||
Remove Local Key
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<h3>Your PGP Keys</h3>
|
||||
|
||||
{#if pgpKeys.length > 0}
|
||||
|
|
@ -1284,58 +1414,116 @@ async function updateColor() {
|
|||
|
||||
{#if showGeneratedKeys}
|
||||
<div style="margin: 2rem 0; padding: 1.5rem; background: rgba(86, 29, 94, 0.1); border: 1px solid rgba(86, 29, 94, 0.3); border-radius: 8px;">
|
||||
<h3>Generated Key Pair</h3>
|
||||
<p style="color: var(--error); margin-bottom: 1rem;">
|
||||
<strong>Important:</strong> Save your private key securely before proceeding.
|
||||
</p>
|
||||
<h3>Generate New Key Pair</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Public Key</label>
|
||||
<textarea
|
||||
readonly
|
||||
rows="8"
|
||||
value={generatedPublicKey}
|
||||
style="font-family: monospace; font-size: 0.8rem;"
|
||||
/>
|
||||
<div style="display: flex; gap: 0.5rem; margin-top: 0.5rem;">
|
||||
<button type="button" on:click={() => copyToClipboard(generatedPublicKey)}>
|
||||
Copy
|
||||
{#if !generatedPrivateKey}
|
||||
<div class="passphrase-info">
|
||||
<h4>🔐 Passphrase Requirements</h4>
|
||||
<ul>
|
||||
<li>At least 12 characters long</li>
|
||||
<li>Include 3 of: uppercase, lowercase, numbers, special characters</li>
|
||||
<li>This passphrase encrypts your private key</li>
|
||||
<li>You'll need it every time you use your key</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="key-passphrase">Passphrase</label>
|
||||
<input
|
||||
type="password"
|
||||
id="key-passphrase"
|
||||
bind:value={keyPassphrase}
|
||||
placeholder="Enter a strong passphrase"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="confirm-key-passphrase">Confirm Passphrase</label>
|
||||
<input
|
||||
type="password"
|
||||
id="confirm-key-passphrase"
|
||||
bind:value={confirmKeyPassphrase}
|
||||
placeholder="Confirm your passphrase"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 1rem;">
|
||||
<button on:click={generateKeysWithPassphrase} disabled={loading || !keyPassphrase}>
|
||||
{loading ? 'Generating...' : 'Generate Keys'}
|
||||
</button>
|
||||
<button type="button" class="btn-secondary" on:click={() => downloadKey(generatedPublicKey, `${displayUsername}-public-key.asc`)}>
|
||||
Download
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
on:click={() => { showGeneratedKeys = false; keyPassphrase = ''; confirmKeyPassphrase = ''; }}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Private Key</label>
|
||||
<textarea
|
||||
readonly
|
||||
rows="8"
|
||||
value={generatedPrivateKey}
|
||||
style="font-family: monospace; font-size: 0.8rem;"
|
||||
/>
|
||||
<div style="display: flex; gap: 0.5rem; margin-top: 0.5rem;">
|
||||
<button type="button" on:click={() => copyToClipboard(generatedPrivateKey)}>
|
||||
Copy
|
||||
{:else}
|
||||
<p style="color: var(--error); margin-bottom: 1rem;">
|
||||
<strong>Important:</strong> Save your private key securely. You will need it and your passphrase to login with PGP.
|
||||
</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Public Key</label>
|
||||
<textarea
|
||||
readonly
|
||||
rows="8"
|
||||
value={generatedPublicKey}
|
||||
style="font-family: monospace; font-size: 0.8rem;"
|
||||
/>
|
||||
<div style="display: flex; gap: 0.5rem; margin-top: 0.5rem;">
|
||||
<button type="button" on:click={() => copyToClipboard(generatedPublicKey)}>
|
||||
Copy
|
||||
</button>
|
||||
<button type="button" class="btn-secondary" on:click={() => downloadKey(generatedPublicKey, `${displayUsername}-public-key.asc`)}>
|
||||
Download
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Private Key (Encrypted with your passphrase)</label>
|
||||
<textarea
|
||||
readonly
|
||||
rows="8"
|
||||
value={generatedPrivateKey}
|
||||
style="font-family: monospace; font-size: 0.8rem;"
|
||||
/>
|
||||
<div style="display: flex; gap: 0.5rem; margin-top: 0.5rem;">
|
||||
<button type="button" on:click={() => copyToClipboard(generatedPrivateKey)}>
|
||||
Copy
|
||||
</button>
|
||||
<button type="button" class="btn-secondary" on:click={() => downloadKey(generatedPrivateKey, `${displayUsername}-private-key.asc`)}>
|
||||
Download (Required!)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="key-storage-option">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="save-key-locally"
|
||||
bind:checked={saveKeyLocally}
|
||||
/>
|
||||
<label for="save-key-locally">
|
||||
Also save encrypted key in this browser for easier PGP login (you can export/remove it later)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 1rem; margin-top: 1rem;">
|
||||
<button on:click={saveGeneratedKeys} disabled={loading}>
|
||||
Save Keys
|
||||
</button>
|
||||
<button type="button" class="btn-secondary" on:click={() => downloadKey(generatedPrivateKey, `${displayUsername}-private-key.asc`)}>
|
||||
Download
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
on:click={() => { showGeneratedKeys = false; generatedPrivateKey = ''; generatedPublicKey = ''; keyPassphrase = ''; confirmKeyPassphrase = ''; }}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 1rem; margin-top: 1rem;">
|
||||
<button on:click={saveGeneratedKeys} disabled={loading}>
|
||||
Save Keys
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
on:click={() => { showGeneratedKeys = false; generatedPrivateKey = ''; generatedPublicKey = ''; }}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
|
||||
|
|
@ -1435,4 +1623,42 @@ async function updateColor() {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showUnlockDialog}
|
||||
<div class="warning-modal">
|
||||
<div class="warning-content">
|
||||
<h2>Unlock Private Key</h2>
|
||||
<p style="margin-bottom: 1rem;">Enter your passphrase to decrypt and export your private key.</p>
|
||||
|
||||
{#if error}
|
||||
<div class="error" style="margin-bottom: 1rem;">{error}</div>
|
||||
{/if}
|
||||
|
||||
<div class="form-group">
|
||||
<label for="unlock-passphrase">Passphrase</label>
|
||||
<input
|
||||
type="password"
|
||||
id="unlock-passphrase"
|
||||
bind:value={unlockPassphrase}
|
||||
placeholder="Enter your key passphrase"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="warning-actions">
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
on:click={() => { showUnlockDialog = false; unlockPassphrase = ''; }}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
on:click={unlockAndExportKey}
|
||||
disabled={!unlockPassphrase}
|
||||
>
|
||||
Unlock & Export
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
Loading…
Add table
Add a link
Reference in a new issue