242 lines
7.0 KiB
TypeScript
242 lines
7.0 KiB
TypeScript
import { stringToBytes, concatBytes, Serializable, AsBytes, AsDate, AsSet, Exclude } from "./encoding";
|
|
import { RSAPublicKey, RSAPrivateKey, RSAKeyParams, HMACKey, HMACParams, HMACKeyParams } from "./crypto";
|
|
import { getCryptoProvider as getProvider } from "./platform";
|
|
import { Err, ErrorCode } from "./error";
|
|
import { PBES2Container } from "./container";
|
|
import { Storable } from "./storage";
|
|
import { VaultID } from "./vault";
|
|
import { Org, OrgInfo } from "./org";
|
|
import { VaultItemID } from "./item";
|
|
|
|
/** Unique identifier for [[Account]] objects */
|
|
export type AccountID = string;
|
|
|
|
export class AccountSecrets extends Serializable {
|
|
constructor({ signingKey, privateKey, favorites }: Partial<AccountSecrets> = {}) {
|
|
super();
|
|
Object.assign(this, { signingKey, privateKey, favorites });
|
|
}
|
|
|
|
@AsBytes()
|
|
signingKey!: Uint8Array;
|
|
|
|
@AsBytes()
|
|
privateKey!: Uint8Array;
|
|
|
|
@AsSet()
|
|
favorites = new Set<VaultItemID>();
|
|
}
|
|
|
|
/**
|
|
* The `Account` object represents an individual Padloc user and holds general
|
|
* account information as well as cryptographic keys necessary for accessing
|
|
* [[Vaults]] and signing/verifying [[Org]]anization details.
|
|
*
|
|
* The [[privateKey]] and [[signingKey]] properties are considered secret and
|
|
* therefore need to be encrypted at rest. For this, the [[Account]] object
|
|
* serves as a [[PBESContainer]] which is unlocked by the users **master
|
|
* password**.
|
|
*/
|
|
export class Account extends PBES2Container implements Storable {
|
|
/** Unique account ID */
|
|
id: AccountID = "";
|
|
|
|
/** The users email address */
|
|
email = "";
|
|
|
|
/** The users display name */
|
|
name = "";
|
|
|
|
/** When the account was created */
|
|
@AsDate()
|
|
created = new Date();
|
|
|
|
/** when the account was last updated */
|
|
@AsDate()
|
|
updated = new Date();
|
|
|
|
/** The accounts public key */
|
|
@AsBytes()
|
|
publicKey!: RSAPublicKey;
|
|
|
|
/**
|
|
* The accounts private key
|
|
*
|
|
* @secret
|
|
* **IMPORTANT**: This property is considered **secret**
|
|
* and should never stored or transmitted in plain text
|
|
*/
|
|
@Exclude()
|
|
privateKey?: RSAPrivateKey;
|
|
|
|
/**
|
|
* HMAC key used for signing and verifying organization details
|
|
*
|
|
* **IMPORTANT**: This property is considered **secret**
|
|
* and should never stored or transmitted in plain text
|
|
*
|
|
* @secret
|
|
*/
|
|
@Exclude()
|
|
signingKey?: HMACKey;
|
|
|
|
/** ID of the accounts main or "private" [[Vault]]. */
|
|
mainVault: {
|
|
id: VaultID;
|
|
name?: string;
|
|
revision?: string;
|
|
} = { id: "" };
|
|
|
|
/** All organizations this account is a member of */
|
|
orgs: OrgInfo[] = [];
|
|
|
|
/**
|
|
* Revision id used for ensuring continuity when synchronizing the account
|
|
* object between client and server
|
|
*/
|
|
revision: string = "";
|
|
|
|
@Exclude()
|
|
favorites = new Set<VaultItemID>();
|
|
|
|
/**
|
|
* Whether or not this Account object is current "locked" or, in other words,
|
|
* whether the `privateKey` and `signingKey` properties have been decrypted.
|
|
*/
|
|
get locked(): boolean {
|
|
return !this.privateKey;
|
|
}
|
|
|
|
get masterKey() {
|
|
return this._key;
|
|
}
|
|
|
|
set masterKey(key: Uint8Array | undefined) {
|
|
this._key = key;
|
|
}
|
|
|
|
/**
|
|
* Generates the accounts [[privateKey]], [[publicKey]] and [[signingKey]] and
|
|
* encrypts [[privateKey]] and [[singingKey]] using the master password.
|
|
*/
|
|
async initialize(password: string) {
|
|
const { publicKey, privateKey } = await getProvider().generateKey(new RSAKeyParams());
|
|
this.publicKey = publicKey;
|
|
this.privateKey = privateKey;
|
|
this.signingKey = await getProvider().generateKey(new HMACKeyParams());
|
|
await this.setPassword(password);
|
|
}
|
|
|
|
/** Updates the master password by reencrypting the [[privateKey]] and [[signingKey]] properties */
|
|
async setPassword(password: string) {
|
|
await super.unlock(password);
|
|
await this._commitSecrets();
|
|
this.updated = new Date();
|
|
}
|
|
|
|
/**
|
|
* "Unlocks" the account by decrypting and extracting [[privateKey]] and
|
|
* [[signingKey]] from [[encryptedData]]
|
|
*/
|
|
async unlock(password: string) {
|
|
await super.unlock(password);
|
|
await this._loadSecrets();
|
|
}
|
|
|
|
/**
|
|
* Unlocks the account by providing the encryption key directly rather than
|
|
* deriving it fro the master password
|
|
*/
|
|
async unlockWithMasterKey(key: Uint8Array) {
|
|
this._key = key;
|
|
await this._loadSecrets();
|
|
}
|
|
|
|
/**
|
|
* "Locks" the account by deleting all sensitive data from the object
|
|
*/
|
|
lock() {
|
|
super.lock();
|
|
delete this.privateKey;
|
|
delete this.signingKey;
|
|
this.favorites.clear();
|
|
}
|
|
|
|
clone() {
|
|
const clone = super.clone();
|
|
clone.copySecrets(this);
|
|
return clone;
|
|
}
|
|
|
|
toString() {
|
|
return this.name || this.email;
|
|
}
|
|
|
|
/**
|
|
* Creates a signature that can be used later to verify an organizations id and public key
|
|
*/
|
|
async signOrg({ id, publicKey }: { id: string; publicKey: Uint8Array }) {
|
|
if (!this.signingKey) {
|
|
throw "Signing key not available!";
|
|
}
|
|
return getProvider().sign(this.signingKey, concatBytes([stringToBytes(id), publicKey], 0x00), new HMACParams());
|
|
}
|
|
|
|
/**
|
|
* Verifies an organizations id an public key, using the signature stored
|
|
* in the [[Member]] object associated with the account.
|
|
*/
|
|
async verifyOrg(org: Org): Promise<void> {
|
|
if (!this.signingKey) {
|
|
throw "Account needs to be unlocked first";
|
|
}
|
|
|
|
const member = org.getMember(this);
|
|
|
|
if (!member) {
|
|
throw new Err(ErrorCode.VERIFICATION_ERROR, "Account is not a member.");
|
|
}
|
|
|
|
const verified = await getProvider().verify(
|
|
this.signingKey,
|
|
member.orgSignature,
|
|
concatBytes([stringToBytes(org.id), org.publicKey], 0x00),
|
|
new HMACParams()
|
|
);
|
|
|
|
if (!verified) {
|
|
throw new Err(ErrorCode.VERIFICATION_ERROR, `Failed to verify public key of ${org.name}!`);
|
|
}
|
|
}
|
|
|
|
async toggleFavorite(id: VaultItemID, favorite: boolean) {
|
|
favorite ? this.favorites.add(id) : this.favorites.delete(id);
|
|
await this._commitSecrets();
|
|
}
|
|
|
|
copySecrets(account: Account) {
|
|
this.privateKey = account.privateKey;
|
|
this.signingKey = account.signingKey;
|
|
this.favorites = account.favorites;
|
|
this._key = account._key;
|
|
}
|
|
|
|
private async _loadSecrets() {
|
|
const secrets = new AccountSecrets().fromBytes(await this.getData());
|
|
if (!secrets.favorites) {
|
|
secrets.favorites = new Set<VaultItemID>();
|
|
}
|
|
Object.assign(this, secrets);
|
|
}
|
|
|
|
private async _commitSecrets() {
|
|
const secrets = new AccountSecrets(this as UnlockedAccount);
|
|
await this.setData(secrets.toBytes());
|
|
}
|
|
}
|
|
|
|
export interface UnlockedAccount extends Account {
|
|
privateKey: Uint8Array;
|
|
signingKey: Uint8Array;
|
|
}
|