feat(api): add CryptoService for secret encryption (MS22-P1b)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
This commit is contained in:
71
apps/api/src/crypto/crypto.service.spec.ts
Normal file
71
apps/api/src/crypto/crypto.service.spec.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { CryptoService } from "./crypto.service";
|
||||
|
||||
function createConfigService(secret?: string): ConfigService {
|
||||
return {
|
||||
get: (key: string) => {
|
||||
if (key === "MOSAIC_SECRET_KEY") {
|
||||
return secret;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
} as unknown as ConfigService;
|
||||
}
|
||||
|
||||
describe("CryptoService", () => {
|
||||
let service: CryptoService;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new CryptoService(createConfigService("this-is-a-test-secret-key-with-32+chars"));
|
||||
});
|
||||
|
||||
it("encrypt -> decrypt roundtrip", () => {
|
||||
const plaintext = "my-secret-api-key";
|
||||
|
||||
const encrypted = service.encrypt(plaintext);
|
||||
const decrypted = service.decrypt(encrypted);
|
||||
|
||||
expect(encrypted.startsWith("enc:")).toBe(true);
|
||||
expect(decrypted).toBe(plaintext);
|
||||
});
|
||||
|
||||
it("decrypt rejects tampered ciphertext", () => {
|
||||
const encrypted = service.encrypt("sensitive-token");
|
||||
const payload = encrypted.slice(4);
|
||||
const bytes = Buffer.from(payload, "base64");
|
||||
|
||||
bytes[bytes.length - 1] = bytes[bytes.length - 1]! ^ 0xff;
|
||||
|
||||
const tampered = `enc:${bytes.toString("base64")}`;
|
||||
|
||||
expect(() => service.decrypt(tampered)).toThrow();
|
||||
});
|
||||
|
||||
it("decrypt rejects non-encrypted string", () => {
|
||||
expect(() => service.decrypt("plain-text-value")).toThrow();
|
||||
});
|
||||
|
||||
it("isEncrypted detects prefix correctly", () => {
|
||||
expect(service.isEncrypted("enc:abc")).toBe(true);
|
||||
expect(service.isEncrypted("ENC:abc")).toBe(false);
|
||||
expect(service.isEncrypted("plain-text")).toBe(false);
|
||||
});
|
||||
|
||||
it("generateToken returns 64-char hex string", () => {
|
||||
const token = service.generateToken();
|
||||
|
||||
expect(token).toMatch(/^[0-9a-f]{64}$/);
|
||||
});
|
||||
|
||||
it("different plaintexts produce different ciphertexts (random IV)", () => {
|
||||
const encryptedA = service.encrypt("value-a");
|
||||
const encryptedB = service.encrypt("value-b");
|
||||
|
||||
expect(encryptedA).not.toBe(encryptedB);
|
||||
});
|
||||
|
||||
it("missing MOSAIC_SECRET_KEY throws on construction", () => {
|
||||
expect(() => new CryptoService(createConfigService(undefined))).toThrow();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user