mirror of
https://github.com/viktoruj/obsidian-cloud-kms.git
synced 2026-07-22 06:56:21 +00:00
317 lines
11 KiB
TypeScript
317 lines
11 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { serialize } from '../../../src/format/serializer';
|
|
import {
|
|
validateMagic,
|
|
validateVersion,
|
|
validateProviderId,
|
|
validateCmkId,
|
|
validateWrappedDek,
|
|
validateNonce,
|
|
validateAuthTag,
|
|
validateCiphertext,
|
|
validateRecord,
|
|
} from '../../../src/format/validators';
|
|
import { MAGIC_BYTES, FORMAT_VERSION, NONCE_LEN, AUTH_TAG_LEN } from '../../../src/constants';
|
|
import type { EncryptedFileRecord } from '../../../src/types';
|
|
|
|
/** Helper to create a valid EncryptedFileRecord for testing */
|
|
function makeValidRecord(overrides?: Partial<EncryptedFileRecord>): EncryptedFileRecord {
|
|
return {
|
|
magic: new Uint8Array(MAGIC_BYTES),
|
|
version: FORMAT_VERSION,
|
|
providerId: 'aws-kms',
|
|
cmkId: 'arn:aws:kms:us-east-1:123456789012:key/test-key-id',
|
|
wrappedDek: new Uint8Array(256).fill(0xAB),
|
|
nonce: new Uint8Array(NONCE_LEN).fill(0x01),
|
|
authTag: new Uint8Array(AUTH_TAG_LEN).fill(0x02),
|
|
ciphertext: new Uint8Array([0x10, 0x20, 0x30, 0x40]),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('serialize', () => {
|
|
it('should serialize a valid record into the correct binary format', () => {
|
|
const record = makeValidRecord();
|
|
const result = serialize(record);
|
|
|
|
const encoder = new TextEncoder();
|
|
const providerIdBytes = encoder.encode(record.providerId);
|
|
const cmkIdBytes = encoder.encode(record.cmkId);
|
|
const N = providerIdBytes.length;
|
|
const M = cmkIdBytes.length;
|
|
const W = record.wrappedDek.length;
|
|
const C = record.ciphertext.length;
|
|
|
|
// Total size check
|
|
expect(result.length).toBe(43 + N + M + W + C);
|
|
|
|
const view = new DataView(result.buffer);
|
|
let offset = 0;
|
|
|
|
// Magic bytes
|
|
expect(result.slice(0, 4)).toEqual(MAGIC_BYTES);
|
|
offset += 4;
|
|
|
|
// Version (uint16 BE)
|
|
expect(view.getUint16(offset, false)).toBe(FORMAT_VERSION);
|
|
offset += 2;
|
|
|
|
// ProviderIdLen
|
|
expect(result[offset]).toBe(N);
|
|
offset += 1;
|
|
|
|
// ProviderId
|
|
expect(result.slice(offset, offset + N)).toEqual(providerIdBytes);
|
|
offset += N;
|
|
|
|
// CmkIdLen (uint16 BE)
|
|
expect(view.getUint16(offset, false)).toBe(M);
|
|
offset += 2;
|
|
|
|
// CmkId
|
|
expect(result.slice(offset, offset + M)).toEqual(cmkIdBytes);
|
|
offset += M;
|
|
|
|
// WrappedDekLen (uint16 BE)
|
|
expect(view.getUint16(offset, false)).toBe(W);
|
|
offset += 2;
|
|
|
|
// WrappedDek
|
|
expect(result.slice(offset, offset + W)).toEqual(record.wrappedDek);
|
|
offset += W;
|
|
|
|
// Nonce (12 bytes)
|
|
expect(result.slice(offset, offset + 12)).toEqual(record.nonce);
|
|
offset += 12;
|
|
|
|
// AuthTag (16 bytes)
|
|
expect(result.slice(offset, offset + 16)).toEqual(record.authTag);
|
|
offset += 16;
|
|
|
|
// CiphertextLen (uint32 BE)
|
|
expect(view.getUint32(offset, false)).toBe(C);
|
|
offset += 4;
|
|
|
|
// Ciphertext
|
|
expect(result.slice(offset, offset + C)).toEqual(record.ciphertext);
|
|
});
|
|
|
|
it('should serialize a record with empty ciphertext', () => {
|
|
const record = makeValidRecord({ ciphertext: new Uint8Array(0) });
|
|
const result = serialize(record);
|
|
|
|
const encoder = new TextEncoder();
|
|
const N = encoder.encode(record.providerId).length;
|
|
const M = encoder.encode(record.cmkId).length;
|
|
const W = record.wrappedDek.length;
|
|
|
|
expect(result.length).toBe(43 + N + M + W + 0);
|
|
|
|
// CiphertextLen should be 0
|
|
const view = new DataView(result.buffer);
|
|
const ciphertextLenOffset = 39 + N + M + W;
|
|
expect(view.getUint32(ciphertextLenOffset, false)).toBe(0);
|
|
});
|
|
|
|
it('should serialize a record with minimum-length fields', () => {
|
|
const record = makeValidRecord({
|
|
providerId: 'a',
|
|
cmkId: 'k',
|
|
wrappedDek: new Uint8Array([0xFF]),
|
|
ciphertext: new Uint8Array(0),
|
|
});
|
|
const result = serialize(record);
|
|
|
|
// 43 + 1 + 1 + 1 + 0 = 46
|
|
expect(result.length).toBe(46);
|
|
});
|
|
|
|
it('should throw on invalid magic bytes', () => {
|
|
const record = makeValidRecord({ magic: new Uint8Array([0x00, 0x00, 0x00, 0x00]) });
|
|
expect(() => serialize(record)).toThrow('Invalid magic bytes');
|
|
});
|
|
|
|
it('should throw on unsupported version', () => {
|
|
const record = makeValidRecord({ version: 99 });
|
|
expect(() => serialize(record)).toThrow('Unsupported format version');
|
|
});
|
|
|
|
it('should throw on empty provider ID', () => {
|
|
const record = makeValidRecord({ providerId: '' });
|
|
expect(() => serialize(record)).toThrow('Invalid provider ID length');
|
|
});
|
|
|
|
it('should throw on provider ID with invalid characters', () => {
|
|
const record = makeValidRecord({ providerId: 'AWS_KMS' });
|
|
expect(() => serialize(record)).toThrow('Invalid provider ID charset');
|
|
});
|
|
|
|
it('should throw on empty CMK ID', () => {
|
|
const record = makeValidRecord({ cmkId: '' });
|
|
expect(() => serialize(record)).toThrow('Invalid CMK ID length');
|
|
});
|
|
|
|
it('should throw on empty wrapped DEK', () => {
|
|
const record = makeValidRecord({ wrappedDek: new Uint8Array(0) });
|
|
expect(() => serialize(record)).toThrow('Invalid wrapped DEK length');
|
|
});
|
|
|
|
it('should throw on wrong nonce length', () => {
|
|
const record = makeValidRecord({ nonce: new Uint8Array(10) });
|
|
expect(() => serialize(record)).toThrow('Invalid nonce length');
|
|
});
|
|
|
|
it('should throw on wrong auth tag length', () => {
|
|
const record = makeValidRecord({ authTag: new Uint8Array(8) });
|
|
expect(() => serialize(record)).toThrow('Invalid auth tag length');
|
|
});
|
|
|
|
it('should throw on ciphertext exceeding max length', () => {
|
|
// We can't allocate 64 MiB + 1 in a test, so test the validator directly
|
|
// This test verifies the serializer calls validation
|
|
const record = makeValidRecord({ wrappedDek: new Uint8Array(1025) });
|
|
expect(() => serialize(record)).toThrow('Invalid wrapped DEK length');
|
|
});
|
|
});
|
|
|
|
describe('validators', () => {
|
|
describe('validateMagic', () => {
|
|
it('should accept valid magic bytes', () => {
|
|
expect(() => validateMagic(new Uint8Array(MAGIC_BYTES))).not.toThrow();
|
|
});
|
|
|
|
it('should reject wrong length', () => {
|
|
expect(() => validateMagic(new Uint8Array([0x4F, 0x43, 0x4B]))).toThrow('Invalid magic bytes length');
|
|
});
|
|
|
|
it('should reject wrong content', () => {
|
|
expect(() => validateMagic(new Uint8Array([0x4F, 0x43, 0x4B, 0x00]))).toThrow('Invalid magic bytes');
|
|
});
|
|
});
|
|
|
|
describe('validateVersion', () => {
|
|
it('should accept version 1', () => {
|
|
expect(() => validateVersion(1)).not.toThrow();
|
|
});
|
|
|
|
it('should reject version 0', () => {
|
|
expect(() => validateVersion(0)).toThrow('version must be at least 1');
|
|
});
|
|
|
|
it('should reject version greater than supported', () => {
|
|
expect(() => validateVersion(2)).toThrow('Unsupported format version');
|
|
});
|
|
|
|
it('should reject negative version', () => {
|
|
expect(() => validateVersion(-1)).toThrow('must be a uint16');
|
|
});
|
|
|
|
it('should reject non-integer version', () => {
|
|
expect(() => validateVersion(1.5)).toThrow('must be a uint16');
|
|
});
|
|
});
|
|
|
|
describe('validateProviderId', () => {
|
|
it('should accept valid provider IDs', () => {
|
|
expect(() => validateProviderId('aws-kms')).not.toThrow();
|
|
expect(() => validateProviderId('azure-key-vault')).not.toThrow();
|
|
expect(() => validateProviderId('gcp-kms')).not.toThrow();
|
|
expect(() => validateProviderId('a')).not.toThrow();
|
|
expect(() => validateProviderId('a'.repeat(32))).not.toThrow();
|
|
});
|
|
|
|
it('should reject empty provider ID', () => {
|
|
expect(() => validateProviderId('')).toThrow('Invalid provider ID length');
|
|
});
|
|
|
|
it('should reject provider ID exceeding max length', () => {
|
|
expect(() => validateProviderId('a'.repeat(33))).toThrow('Invalid provider ID length');
|
|
});
|
|
|
|
it('should reject uppercase characters', () => {
|
|
expect(() => validateProviderId('AWS-KMS')).toThrow('Invalid provider ID charset');
|
|
});
|
|
|
|
it('should reject underscores', () => {
|
|
expect(() => validateProviderId('aws_kms')).toThrow('Invalid provider ID charset');
|
|
});
|
|
|
|
it('should reject spaces', () => {
|
|
expect(() => validateProviderId('aws kms')).toThrow('Invalid provider ID charset');
|
|
});
|
|
});
|
|
|
|
describe('validateCmkId', () => {
|
|
it('should accept valid CMK IDs', () => {
|
|
expect(() => validateCmkId('arn:aws:kms:us-east-1:123456789012:key/test')).not.toThrow();
|
|
expect(() => validateCmkId('k')).not.toThrow();
|
|
});
|
|
|
|
it('should reject empty CMK ID', () => {
|
|
expect(() => validateCmkId('')).toThrow('Invalid CMK ID length');
|
|
});
|
|
|
|
it('should reject CMK ID exceeding max length', () => {
|
|
expect(() => validateCmkId('x'.repeat(2049))).toThrow('Invalid CMK ID length');
|
|
});
|
|
});
|
|
|
|
describe('validateWrappedDek', () => {
|
|
it('should accept valid wrapped DEK', () => {
|
|
expect(() => validateWrappedDek(new Uint8Array(1))).not.toThrow();
|
|
expect(() => validateWrappedDek(new Uint8Array(1024))).not.toThrow();
|
|
});
|
|
|
|
it('should reject empty wrapped DEK', () => {
|
|
expect(() => validateWrappedDek(new Uint8Array(0))).toThrow('Invalid wrapped DEK length');
|
|
});
|
|
|
|
it('should reject wrapped DEK exceeding max length', () => {
|
|
expect(() => validateWrappedDek(new Uint8Array(1025))).toThrow('Invalid wrapped DEK length');
|
|
});
|
|
});
|
|
|
|
describe('validateNonce', () => {
|
|
it('should accept 12-byte nonce', () => {
|
|
expect(() => validateNonce(new Uint8Array(12))).not.toThrow();
|
|
});
|
|
|
|
it('should reject wrong length', () => {
|
|
expect(() => validateNonce(new Uint8Array(11))).toThrow('Invalid nonce length');
|
|
expect(() => validateNonce(new Uint8Array(13))).toThrow('Invalid nonce length');
|
|
});
|
|
});
|
|
|
|
describe('validateAuthTag', () => {
|
|
it('should accept 16-byte auth tag', () => {
|
|
expect(() => validateAuthTag(new Uint8Array(16))).not.toThrow();
|
|
});
|
|
|
|
it('should reject wrong length', () => {
|
|
expect(() => validateAuthTag(new Uint8Array(15))).toThrow('Invalid auth tag length');
|
|
expect(() => validateAuthTag(new Uint8Array(17))).toThrow('Invalid auth tag length');
|
|
});
|
|
});
|
|
|
|
describe('validateCiphertext', () => {
|
|
it('should accept empty ciphertext', () => {
|
|
expect(() => validateCiphertext(new Uint8Array(0))).not.toThrow();
|
|
});
|
|
|
|
it('should accept ciphertext within limit', () => {
|
|
expect(() => validateCiphertext(new Uint8Array(100))).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('validateRecord', () => {
|
|
it('should accept a fully valid record', () => {
|
|
const record = makeValidRecord();
|
|
expect(() => validateRecord(record)).not.toThrow();
|
|
});
|
|
|
|
it('should throw on first invalid field', () => {
|
|
const record = makeValidRecord({ magic: new Uint8Array(4) });
|
|
expect(() => validateRecord(record)).toThrow('magic');
|
|
});
|
|
});
|
|
});
|