- Upgrade vault encryption from PBKDF2 (1000 iterations) to Argon2id
(256MB memory, 8 iterations, 4 threads, ~3 second derivation)
- Add automatic migration from v1 to v2 vault format on unlock
- Add WebAssembly CSP support for hash-wasm Argon2id implementation
- Add NIP-42 relay authentication support for auth-required relays
- Add profile edit feature with pencil icon on identity page
- Add direct NIP-05 validation (removes NDK dependency for validation)
- Add deriving modal with progress timer during key derivation
- Add client tag "plebeian-signer" to profile events
- Fix modal colors (dark theme for visibility)
- Fix NIP-05 badge styling to include check/error indicator
- Add release zip packages for Chrome and Firefox
New files:
- projects/common/src/lib/helpers/argon2-crypto.ts
- projects/common/src/lib/helpers/websocket-auth.ts
- projects/common/src/lib/helpers/nip05-validator.ts
- projects/common/src/lib/components/deriving-modal/
- projects/{chrome,firefox}/src/app/components/profile-edit/
- releases/plebeian-signer-{chrome,firefox}-v1.0.0.zip
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
274 lines
7.9 KiB
TypeScript
274 lines
7.9 KiB
TypeScript
import {
|
|
CryptoHelper,
|
|
Identity_DECRYPTED,
|
|
Identity_ENCRYPTED,
|
|
NostrHelper,
|
|
StorageService,
|
|
} from '@common';
|
|
|
|
export const addIdentity = async function (
|
|
this: StorageService,
|
|
data: {
|
|
nick: string;
|
|
privkeyString: string;
|
|
}
|
|
): Promise<void> {
|
|
this.assureIsInitialized();
|
|
|
|
const privkey = NostrHelper.getNostrPrivkeyObject(
|
|
data.privkeyString.toLowerCase()
|
|
).hex;
|
|
|
|
// Check if an identity with the same privkey already exists.
|
|
const existingIdentity = (
|
|
this.getBrowserSessionHandler().browserSessionData?.identities ?? []
|
|
).find((x) => x.privkey === privkey);
|
|
if (existingIdentity) {
|
|
throw new Error(
|
|
`An identity with the same private key already exists: ${existingIdentity.nick}`
|
|
);
|
|
}
|
|
|
|
const browserSessionData = this.getBrowserSessionHandler().browserSessionData;
|
|
if (!browserSessionData) {
|
|
throw new Error('Browser session data is undefined.');
|
|
}
|
|
|
|
const decryptedIdentity: Identity_DECRYPTED = {
|
|
id: CryptoHelper.v4(),
|
|
nick: data.nick,
|
|
privkey,
|
|
createdAt: new Date().toISOString(),
|
|
};
|
|
|
|
// Add the new identity to the session data.
|
|
browserSessionData.identities.push(decryptedIdentity);
|
|
let isFirstIdentity = false;
|
|
if (browserSessionData.identities.length === 1) {
|
|
isFirstIdentity = true;
|
|
browserSessionData.selectedIdentityId = decryptedIdentity.id;
|
|
}
|
|
this.getBrowserSessionHandler().saveFullData(browserSessionData);
|
|
|
|
// Encrypt the new identity and add it to the sync data.
|
|
const encryptedIdentity = await encryptIdentity.call(this, decryptedIdentity);
|
|
const encryptedIdentities = [
|
|
...(this.getBrowserSyncHandler().browserSyncData?.identities ?? []),
|
|
encryptedIdentity,
|
|
];
|
|
|
|
await this.getBrowserSyncHandler().saveAndSetPartialData_Identities({
|
|
identities: encryptedIdentities,
|
|
});
|
|
|
|
if (isFirstIdentity) {
|
|
await this.getBrowserSyncHandler().saveAndSetPartialData_SelectedIdentityId(
|
|
{
|
|
selectedIdentityId: encryptedIdentity.id,
|
|
}
|
|
);
|
|
}
|
|
};
|
|
|
|
export const deleteIdentity = async function (
|
|
this: StorageService,
|
|
identityId: string | undefined
|
|
): Promise<void> {
|
|
this.assureIsInitialized();
|
|
|
|
if (!identityId) {
|
|
return;
|
|
}
|
|
|
|
const browserSessionData = this.getBrowserSessionHandler().browserSessionData;
|
|
const browserSyncData = this.getBrowserSyncHandler().browserSyncData;
|
|
if (!browserSessionData || !browserSyncData) {
|
|
throw new Error('Browser session or sync data is undefined.');
|
|
}
|
|
|
|
browserSessionData.identities = browserSessionData.identities.filter(
|
|
(x) => x.id !== identityId
|
|
);
|
|
browserSessionData.permissions = browserSessionData.permissions.filter(
|
|
(x) => x.identityId !== identityId
|
|
);
|
|
browserSessionData.relays = browserSessionData.relays.filter(
|
|
(x) => x.identityId !== identityId
|
|
);
|
|
if (browserSessionData.selectedIdentityId === identityId) {
|
|
// Choose another identity to be selected or null if there is none.
|
|
browserSessionData.selectedIdentityId =
|
|
browserSessionData.identities.length > 0
|
|
? browserSessionData.identities[0].id
|
|
: null;
|
|
}
|
|
await this.getBrowserSessionHandler().saveFullData(browserSessionData);
|
|
|
|
// Handle Sync data.
|
|
const encryptedIdentityId = await this.encrypt(identityId);
|
|
await this.getBrowserSyncHandler().saveAndSetPartialData_Identities({
|
|
identities: browserSyncData.identities.filter(
|
|
(x) => x.id !== encryptedIdentityId
|
|
),
|
|
});
|
|
await this.getBrowserSyncHandler().saveAndSetPartialData_Permissions({
|
|
permissions: browserSyncData.permissions.filter(
|
|
(x) => x.identityId !== encryptedIdentityId
|
|
),
|
|
});
|
|
await this.getBrowserSyncHandler().saveAndSetPartialData_Relays({
|
|
relays: browserSyncData.relays.filter(
|
|
(x) => x.identityId !== encryptedIdentityId
|
|
),
|
|
});
|
|
await this.getBrowserSyncHandler().saveAndSetPartialData_SelectedIdentityId({
|
|
selectedIdentityId:
|
|
browserSessionData.selectedIdentityId === null
|
|
? null
|
|
: await this.encrypt(browserSessionData.selectedIdentityId),
|
|
});
|
|
};
|
|
|
|
export const switchIdentity = async function (
|
|
this: StorageService,
|
|
identityId: string | null
|
|
): Promise<void> {
|
|
this.assureIsInitialized();
|
|
|
|
// Check, if the identity really exists.
|
|
const browserSessionData = this.getBrowserSessionHandler().browserSessionData;
|
|
|
|
if (!browserSessionData?.identities.find((x) => x.id === identityId)) {
|
|
return;
|
|
}
|
|
|
|
browserSessionData.selectedIdentityId = identityId;
|
|
await this.getBrowserSessionHandler().saveFullData(browserSessionData);
|
|
|
|
const encryptedIdentityId =
|
|
identityId === null ? null : await this.encrypt(identityId);
|
|
await this.getBrowserSyncHandler().saveAndSetPartialData_SelectedIdentityId({
|
|
selectedIdentityId: encryptedIdentityId,
|
|
});
|
|
};
|
|
|
|
export const encryptIdentity = async function (
|
|
this: StorageService,
|
|
identity: Identity_DECRYPTED
|
|
): Promise<Identity_ENCRYPTED> {
|
|
const encryptedIdentity: Identity_ENCRYPTED = {
|
|
id: await this.encrypt(identity.id),
|
|
nick: await this.encrypt(identity.nick),
|
|
createdAt: await this.encrypt(identity.createdAt),
|
|
privkey: await this.encrypt(identity.privkey),
|
|
};
|
|
|
|
return encryptedIdentity;
|
|
};
|
|
|
|
/**
|
|
* Locked vault context for decryption during unlock
|
|
* - v1 vaults use password (PBKDF2)
|
|
* - v2 vaults use keyBase64 (pre-derived Argon2id key)
|
|
*/
|
|
export type LockedVaultContext =
|
|
| { iv: string; password: string; keyBase64?: undefined }
|
|
| { iv: string; keyBase64: string; password?: undefined };
|
|
|
|
export const decryptIdentities = async function (
|
|
this: StorageService,
|
|
identities: Identity_ENCRYPTED[],
|
|
withLockedVault: LockedVaultContext | undefined = undefined
|
|
): Promise<Identity_DECRYPTED[]> {
|
|
const decryptedIdentities: Identity_DECRYPTED[] = [];
|
|
|
|
for (const identity of identities) {
|
|
const decryptedIdentity = await decryptIdentity.call(
|
|
this,
|
|
identity,
|
|
withLockedVault
|
|
);
|
|
decryptedIdentities.push(decryptedIdentity);
|
|
}
|
|
|
|
return decryptedIdentities;
|
|
};
|
|
|
|
export const decryptIdentity = async function (
|
|
this: StorageService,
|
|
identity: Identity_ENCRYPTED,
|
|
withLockedVault: LockedVaultContext | undefined = undefined
|
|
): Promise<Identity_DECRYPTED> {
|
|
if (typeof withLockedVault === 'undefined') {
|
|
const decryptedIdentity: Identity_DECRYPTED = {
|
|
id: await this.decrypt(identity.id, 'string'),
|
|
nick: await this.decrypt(identity.nick, 'string'),
|
|
createdAt: await this.decrypt(identity.createdAt, 'string'),
|
|
privkey: await this.decrypt(identity.privkey, 'string'),
|
|
};
|
|
|
|
return decryptedIdentity;
|
|
}
|
|
|
|
// v2: Use pre-derived key
|
|
if (withLockedVault.keyBase64) {
|
|
const decryptedIdentity: Identity_DECRYPTED = {
|
|
id: await this.decryptWithLockedVaultV2(
|
|
identity.id,
|
|
'string',
|
|
withLockedVault.iv,
|
|
withLockedVault.keyBase64
|
|
),
|
|
nick: await this.decryptWithLockedVaultV2(
|
|
identity.nick,
|
|
'string',
|
|
withLockedVault.iv,
|
|
withLockedVault.keyBase64
|
|
),
|
|
createdAt: await this.decryptWithLockedVaultV2(
|
|
identity.createdAt,
|
|
'string',
|
|
withLockedVault.iv,
|
|
withLockedVault.keyBase64
|
|
),
|
|
privkey: await this.decryptWithLockedVaultV2(
|
|
identity.privkey,
|
|
'string',
|
|
withLockedVault.iv,
|
|
withLockedVault.keyBase64
|
|
),
|
|
};
|
|
return decryptedIdentity;
|
|
}
|
|
|
|
// v1: Use password (PBKDF2)
|
|
const decryptedIdentity: Identity_DECRYPTED = {
|
|
id: await this.decryptWithLockedVault(
|
|
identity.id,
|
|
'string',
|
|
withLockedVault.iv,
|
|
withLockedVault.password!
|
|
),
|
|
nick: await this.decryptWithLockedVault(
|
|
identity.nick,
|
|
'string',
|
|
withLockedVault.iv,
|
|
withLockedVault.password!
|
|
),
|
|
createdAt: await this.decryptWithLockedVault(
|
|
identity.createdAt,
|
|
'string',
|
|
withLockedVault.iv,
|
|
withLockedVault.password!
|
|
),
|
|
privkey: await this.decryptWithLockedVault(
|
|
identity.privkey,
|
|
'string',
|
|
withLockedVault.iv,
|
|
withLockedVault.password!
|
|
),
|
|
};
|
|
|
|
return decryptedIdentity;
|
|
};
|