mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-01 08:06:13 +00:00
feat: manage public keys and encryption at rest through the Stalwart API
This commit is contained in:
+128
-1
@@ -37,6 +37,30 @@ export interface AuthInfo {
|
|||||||
apiKeys: ApiKeyInfo[];
|
apiKeys: ApiKeyInfo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CryptoInfo {
|
||||||
|
type: EncryptionType;
|
||||||
|
publicKeyId: string | null;
|
||||||
|
encryptOnAppend: boolean;
|
||||||
|
allowSpamTraining: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PublicKeyInfo {
|
||||||
|
id: string;
|
||||||
|
description: string;
|
||||||
|
emailAddresses: string[];
|
||||||
|
createdAt: string | null;
|
||||||
|
expiresAt: string | null;
|
||||||
|
// Key material (PEM / armored). Only returned by some server versions.
|
||||||
|
key?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PublicKeyInput {
|
||||||
|
description: string;
|
||||||
|
key: string;
|
||||||
|
emailAddresses?: string[];
|
||||||
|
expiresAt?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PrincipalInfo {
|
export interface PrincipalInfo {
|
||||||
displayName: string;
|
displayName: string;
|
||||||
emails: string[];
|
emails: string[];
|
||||||
@@ -151,10 +175,113 @@ export async function fetchAuthInfo(): Promise<AuthInfo> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchEncryptionType(): Promise<EncryptionType> {
|
export async function fetchEncryptionType(): Promise<EncryptionType> {
|
||||||
|
return (await fetchCryptoInfo()).type;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchCryptoInfo(): Promise<CryptoInfo> {
|
||||||
const accountId = jmapClient.accountId;
|
const accountId = jmapClient.accountId;
|
||||||
const responses = await send([['x:AccountSettings/get', { accountId, ids: ['singleton'] }, '0']]);
|
const responses = await send([['x:AccountSettings/get', { accountId, ids: ['singleton'] }, '0']]);
|
||||||
const result = resultFor<{ list?: Array<{ encryptionAtRest?: unknown }> }>(responses, '0');
|
const result = resultFor<{ list?: Array<{ encryptionAtRest?: unknown }> }>(responses, '0');
|
||||||
return extractEncryptionType(result.list?.[0]?.encryptionAtRest);
|
const raw = (result.list?.[0]?.encryptionAtRest ?? {}) as {
|
||||||
|
publicKey?: string | null;
|
||||||
|
encryptOnAppend?: boolean;
|
||||||
|
allowSpamTraining?: boolean;
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
type: extractEncryptionType(result.list?.[0]?.encryptionAtRest),
|
||||||
|
publicKeyId: typeof raw.publicKey === 'string' ? raw.publicKey : null,
|
||||||
|
encryptOnAppend: raw.encryptOnAppend === true,
|
||||||
|
allowSpamTraining: raw.allowSpamTraining === true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure Stalwart's encryption at rest (`x:AccountSettings/set`
|
||||||
|
* `encryptionAtRest`). `Aes128`/`Aes256` need a public key id from
|
||||||
|
* {@link fetchPublicKeys}; `encryptOnAppend` also encrypts mail the user
|
||||||
|
* files (sent/imported), `allowSpamTraining` lets the classifier read a
|
||||||
|
* message before it is encrypted.
|
||||||
|
*/
|
||||||
|
export async function updateEncryptionAtRest(input: {
|
||||||
|
type: EncryptionType;
|
||||||
|
publicKeyId?: string | null;
|
||||||
|
encryptOnAppend?: boolean;
|
||||||
|
allowSpamTraining?: boolean;
|
||||||
|
}): Promise<void> {
|
||||||
|
const accountId = jmapClient.accountId;
|
||||||
|
let payload: Record<string, unknown>;
|
||||||
|
if (input.type === 'Disabled') {
|
||||||
|
payload = { '@type': 'Disabled' };
|
||||||
|
} else {
|
||||||
|
if (!input.publicKeyId) throw new Error('A public key is required to enable encryption');
|
||||||
|
payload = {
|
||||||
|
'@type': input.type,
|
||||||
|
publicKey: input.publicKeyId,
|
||||||
|
encryptOnAppend: input.encryptOnAppend ?? false,
|
||||||
|
allowSpamTraining: input.allowSpamTraining ?? false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const responses = await send([
|
||||||
|
['x:AccountSettings/set', { accountId, update: { singleton: { encryptionAtRest: payload } } }, '0'],
|
||||||
|
]);
|
||||||
|
const result = resultFor<{ notUpdated?: Record<string, { description?: string; type?: string }> }>(responses, '0');
|
||||||
|
const failure = result.notUpdated?.singleton;
|
||||||
|
if (failure) throw new Error(failure.description || failure.type || 'Failed to update encryption settings');
|
||||||
|
}
|
||||||
|
|
||||||
|
function publicKeyFromResult(raw: Record<string, unknown>): PublicKeyInfo {
|
||||||
|
const emails =
|
||||||
|
raw.emailAddresses && typeof raw.emailAddresses === 'object'
|
||||||
|
? Object.keys(raw.emailAddresses as Record<string, unknown>)
|
||||||
|
: [];
|
||||||
|
return {
|
||||||
|
id: String(raw.id ?? ''),
|
||||||
|
description: typeof raw.description === 'string' ? raw.description : '',
|
||||||
|
emailAddresses: emails,
|
||||||
|
createdAt: typeof raw.createdAt === 'string' ? raw.createdAt : null,
|
||||||
|
expiresAt: typeof raw.expiresAt === 'string' ? raw.expiresAt : null,
|
||||||
|
key: typeof raw.key === 'string' ? raw.key : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** S/MIME certificates and PGP public keys stored on the server (`x:PublicKey`). */
|
||||||
|
export async function fetchPublicKeys(): Promise<PublicKeyInfo[]> {
|
||||||
|
const accountId = jmapClient.accountId;
|
||||||
|
const queryResponses = await send([['x:PublicKey/query', { accountId }, '0']]);
|
||||||
|
const query = resultFor<{ ids?: string[] }>(queryResponses, '0');
|
||||||
|
if (!query.ids?.length) return [];
|
||||||
|
const getResponses = await send([['x:PublicKey/get', { accountId, ids: query.ids }, '0']]);
|
||||||
|
const got = resultFor<{ list?: Array<Record<string, unknown>> }>(getResponses, '0');
|
||||||
|
return (got.list ?? []).map(publicKeyFromResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createPublicKey(input: PublicKeyInput): Promise<string> {
|
||||||
|
const accountId = jmapClient.accountId;
|
||||||
|
const tmpId = 'new';
|
||||||
|
const payload: Record<string, unknown> = {
|
||||||
|
description: input.description,
|
||||||
|
key: input.key,
|
||||||
|
emailAddresses: ipsToMap(input.emailAddresses) ?? {},
|
||||||
|
};
|
||||||
|
if (input.expiresAt) payload.expiresAt = input.expiresAt;
|
||||||
|
const responses = await send([['x:PublicKey/set', { accountId, create: { [tmpId]: payload } }, '0']]);
|
||||||
|
const result = resultFor<{
|
||||||
|
created?: Record<string, { id: string }>;
|
||||||
|
notCreated?: Record<string, { type?: string; description?: string }>;
|
||||||
|
}>(responses, '0');
|
||||||
|
const notCreated = result.notCreated?.[tmpId];
|
||||||
|
if (notCreated) throw new Error(notCreated.description || notCreated.type || 'Failed to add public key');
|
||||||
|
const created = result.created?.[tmpId];
|
||||||
|
if (!created?.id) throw new Error('Server did not return the created public key');
|
||||||
|
return created.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removePublicKey(id: string): Promise<void> {
|
||||||
|
const accountId = jmapClient.accountId;
|
||||||
|
const responses = await send([['x:PublicKey/set', { accountId, destroy: [id] }, '0']]);
|
||||||
|
const result = resultFor<{ notDestroyed?: Record<string, { description?: string; type?: string }> }>(responses, '0');
|
||||||
|
const failure = result.notDestroyed?.[id];
|
||||||
|
if (failure) throw new Error(failure.description || failure.type || 'Failed to remove public key');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchPrincipal(): Promise<PrincipalInfo> {
|
export async function fetchPrincipal(): Promise<PrincipalInfo> {
|
||||||
|
|||||||
Reference in New Issue
Block a user