Files
plebeian-signer/projects/common/src/lib/services/storage/related/identity.ts
mleku ebe2b695cc Release v1.0.0 - Major security upgrade with Argon2id encryption
- 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>
2025-12-19 12:30:10 +01:00

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;
};