wraith/backend/src/vault/credentials.service.ts
Vantz Stockwell 11e1705110 fix: detect orphaned SSH key references and missing auth methods
When a credential's sshKeyId points to a deleted/missing SSH key row,
the connection attempt silently had zero auth methods. Now throws a
clear error explaining the SSH key is missing. Also catches the case
where a credential has neither password nor SSH key configured.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 01:04:41 -04:00

100 lines
3.2 KiB
TypeScript

import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { EncryptionService } from './encryption.service';
import { CreateCredentialDto } from './dto/create-credential.dto';
import { UpdateCredentialDto } from './dto/update-credential.dto';
@Injectable()
export class CredentialsService {
constructor(
private prisma: PrismaService,
private encryption: EncryptionService,
) {}
findAll() {
return this.prisma.credential.findMany({
include: { sshKey: { select: { id: true, name: true, keyType: true, fingerprint: true } } },
orderBy: { name: 'asc' },
});
}
async findOne(id: number) {
const cred = await this.prisma.credential.findUnique({
where: { id },
include: { sshKey: true, hosts: { select: { id: true, name: true } } },
});
if (!cred) throw new NotFoundException(`Credential ${id} not found`);
return cred;
}
create(dto: CreateCredentialDto) {
const encryptedValue = dto.password ? this.encryption.encrypt(dto.password) : null;
return this.prisma.credential.create({
data: {
name: dto.name,
username: dto.username,
domain: dto.domain,
type: dto.type,
encryptedValue,
sshKeyId: dto.sshKeyId,
},
});
}
async update(id: number, dto: UpdateCredentialDto) {
await this.findOne(id);
const data: any = { ...dto };
delete data.password;
if (dto.password) {
data.encryptedValue = this.encryption.encrypt(dto.password);
}
return this.prisma.credential.update({ where: { id }, data });
}
async remove(id: number) {
await this.findOne(id);
return this.prisma.credential.delete({ where: { id } });
}
/** Decrypt credential for use in SSH/RDP connections. Never expose over API. */
async decryptForConnection(id: number): Promise<{
username: string | null;
domain: string | null;
password: string | null;
sshKey: { privateKey: string; passphrase: string | null } | null;
}> {
const cred = await this.prisma.credential.findUnique({
where: { id },
include: { sshKey: true },
});
if (!cred) throw new NotFoundException(`Credential ${id} not found`);
let password: string | null = null;
if (cred.encryptedValue) {
password = this.encryption.decrypt(cred.encryptedValue);
}
let sshKey: { privateKey: string; passphrase: string | null } | null = null;
if (cred.sshKey) {
const privateKey = this.encryption.decrypt(cred.sshKey.encryptedPrivateKey);
const passphrase = cred.sshKey.passphraseEncrypted
? this.encryption.decrypt(cred.sshKey.passphraseEncrypted)
: null;
sshKey = { privateKey, passphrase };
} else if (cred.sshKeyId) {
// Orphaned reference — credential points to a deleted/missing SSH key
throw new NotFoundException(
`Credential "${cred.name}" references SSH key #${cred.sshKeyId} which no longer exists. Re-import the key or update the credential.`,
);
}
if (!password && !sshKey) {
throw new NotFoundException(
`Credential "${cred.name}" has no password or SSH key configured.`,
);
}
return { username: cred.username, domain: cred.domain, password, sshKey };
}
}