diff --git a/src/value-objects/crypto/PrivateKey.ts b/src/value-objects/crypto/PrivateKey.ts index ed254c0..3a1c5b0 100644 --- a/src/value-objects/crypto/PrivateKey.ts +++ b/src/value-objects/crypto/PrivateKey.ts @@ -3,6 +3,7 @@ import { Buffer } from 'buffer'; import { InvalidFormatError } from '../../errors/InvalidFormatError'; import { InvalidLengthError } from '../../errors/InvalidLengthError'; import { assert } from '../../patterns'; +import { Media } from '../media'; import { NullObject } from '../NullObject'; import { StringValueObject } from '../StringValueObject'; import { CryptoAdapter } from './CryptoAdapter'; @@ -96,7 +97,10 @@ export class PrivateKey extends Key { } public sign(payload: CryptoPayload): Signature { - const messageBuffer = Buffer.from(payload.valueOf()); + const messageBuffer = + payload instanceof Media + ? payload.getBuffer() + : Buffer.from(payload.valueOf()); const signatureBuffer = CryptoAdapter.sign(messageBuffer, this.valueOf()); return Signature.fromBuffer(signatureBuffer); diff --git a/src/value-objects/crypto/PublicKey.ts b/src/value-objects/crypto/PublicKey.ts index 961a132..2af7388 100644 --- a/src/value-objects/crypto/PublicKey.ts +++ b/src/value-objects/crypto/PublicKey.ts @@ -3,6 +3,7 @@ import { Buffer } from 'buffer'; import { InvalidFormatError } from '../../errors/InvalidFormatError'; import { InvalidLengthError } from '../../errors/InvalidLengthError'; import { assert } from '../../patterns'; +import { Media } from '../media'; import { NullObject } from '../NullObject'; import { StringValueObject } from '../StringValueObject'; import { CryptoAdapter } from './CryptoAdapter'; @@ -43,7 +44,10 @@ export class PublicKey extends Key { payload: CryptoPayload, signature: Signature, ): boolean { - const messageBuffer = Buffer.from(payload.valueOf()); + const messageBuffer = + payload instanceof Media + ? payload.getBuffer() + : Buffer.from(payload.valueOf()); const signatureBuffer = Buffer.from(signature.valueOf(), 'base64'); const valid = CryptoAdapter.verify( signatureBuffer, @@ -55,7 +59,11 @@ export class PublicKey extends Key { } public encrypt(payload: CryptoPayload): EncryptedPayload { - const messageBuffer = Buffer.from(payload.valueOf()); + const messageBuffer = + payload instanceof Media + ? payload.getBuffer() + : Buffer.from(payload.valueOf()); + assert( messageBuffer.length <= PublicKey.MAX_PAYLOAD_LENGTH, new InvalidLengthError( diff --git a/src/value-objects/hashes/MD5Hash.ts b/src/value-objects/hashes/MD5Hash.ts index 703e78f..405b360 100644 --- a/src/value-objects/hashes/MD5Hash.ts +++ b/src/value-objects/hashes/MD5Hash.ts @@ -16,7 +16,12 @@ export class MD5Hash extends Hash { public static from( buffer: string | StringValueObject | Media | Buffer, ): MD5Hash { - return new MD5Hash(CryptoAdapter.hash('md5', buffer.valueOf())); + return new MD5Hash( + CryptoAdapter.hash( + 'md5', + buffer instanceof Media ? buffer.getBuffer() : buffer.valueOf(), + ), + ); } constructor(source: string | StringValueObject) { diff --git a/src/value-objects/hashes/SHA256Hash.ts b/src/value-objects/hashes/SHA256Hash.ts index 9fc206d..a26a625 100644 --- a/src/value-objects/hashes/SHA256Hash.ts +++ b/src/value-objects/hashes/SHA256Hash.ts @@ -16,7 +16,12 @@ export class SHA256Hash extends ValueObject { public static from( buffer: string | StringValueObject | Media | Buffer, ): SHA256Hash { - return new SHA256Hash(CryptoAdapter.hash('sha256', buffer.valueOf())); + return new SHA256Hash( + CryptoAdapter.hash( + 'sha256', + buffer instanceof Media ? buffer.getBuffer() : buffer.valueOf(), + ), + ); } constructor(source: string | StringValueObject) { diff --git a/src/value-objects/hashes/SHA512Hash.ts b/src/value-objects/hashes/SHA512Hash.ts index 8b04f8e..3069a78 100644 --- a/src/value-objects/hashes/SHA512Hash.ts +++ b/src/value-objects/hashes/SHA512Hash.ts @@ -16,7 +16,12 @@ export class SHA512Hash extends ValueObject { public static from( buffer: string | StringValueObject | Media | Buffer, ): SHA512Hash { - return new SHA512Hash(CryptoAdapter.hash('sha512', buffer.valueOf())); + return new SHA512Hash( + CryptoAdapter.hash( + 'sha512', + buffer instanceof Media ? buffer.getBuffer() : buffer.valueOf(), + ), + ); } constructor(source: string | StringValueObject) { diff --git a/src/value-objects/media/Media.ts b/src/value-objects/media/Media.ts index 4dff749..ce676aa 100644 --- a/src/value-objects/media/Media.ts +++ b/src/value-objects/media/Media.ts @@ -4,16 +4,22 @@ import { NullObject } from '../NullObject'; import { ValueObject } from '../ValueObject'; export class Media extends ValueObject { + private readonly buffer?: Buffer; + constructor(value: string | Buffer) { super(value?.toString()); if (NullObject.isNullObject(this)) { return this; } + + this.buffer = Buffer.isBuffer(value) ? Buffer.from(value) : undefined; } public getBuffer(): Buffer { - return Buffer.from(this.value); + return this.buffer === undefined + ? Buffer.from(this.value) + : Buffer.from(this.buffer); } public getSize(): number { @@ -23,4 +29,16 @@ export class Media extends ValueObject { public getBase64(): string { return this.getBuffer().toString('base64'); } + + public isEqual(other: unknown): boolean { + if (other instanceof Media) { + return this.getBuffer().equals(other.getBuffer()); + } + + if (Buffer.isBuffer(other)) { + return this.getBuffer().equals(other); + } + + return super.isEqual(other); + } } diff --git a/tests/value-objects/crypto/PrivateKey.spec.ts b/tests/value-objects/crypto/PrivateKey.spec.ts index 0848171..c058238 100644 --- a/tests/value-objects/crypto/PrivateKey.spec.ts +++ b/tests/value-objects/crypto/PrivateKey.spec.ts @@ -5,6 +5,7 @@ import { InvalidFormatError, InvalidLengthError, Key, + Media, PrivateKey, PublicKey, Signature, @@ -142,6 +143,20 @@ describe('PrivateKey', () => { ); expect(valid).toBeTrue(); }); + + it('should sign raw Media bytes', () => { + const key = new PrivateKey(privatePem); + const payload = Buffer.from([0xff, 0xfe, 0xfd, 0x00, 0x80]); + const sig = key.sign(new Media(payload)); + + const valid = crypto.verify( + null, + payload, + publicPem, + Buffer.from(sig.valueOf(), 'base64'), + ); + expect(valid).toBeTrue(); + }); }); describe('decrypt', () => { diff --git a/tests/value-objects/crypto/PublicKey.spec.ts b/tests/value-objects/crypto/PublicKey.spec.ts index 18a669b..03e7bcb 100644 --- a/tests/value-objects/crypto/PublicKey.spec.ts +++ b/tests/value-objects/crypto/PublicKey.spec.ts @@ -5,6 +5,7 @@ import { InvalidFormatError, InvalidLengthError, Key, + Media, PrivateKey, PublicKey, Signature, @@ -112,6 +113,15 @@ describe('PublicKey', () => { expect(key.isValidSignature(payloadVO, signature)).toBeTrue(); }); + + it('should verify signatures against raw Media bytes', () => { + const key = new PublicKey(publicPem); + const payload = Buffer.from([0xff, 0xfe, 0xfd, 0x00, 0x80]); + const sigBuf = crypto.sign(null, payload, privatePem); + const signature = Signature.fromBuffer(sigBuf); + + expect(key.isValidSignature(new Media(payload), signature)).toBeTrue(); + }); }); describe('encrypt', () => { @@ -146,6 +156,16 @@ describe('PublicKey', () => { expect(encrypted).toBeInstanceOf(EncryptedPayload); }); + it('should encrypt raw Media bytes', () => { + const pubKey = new PublicKey(publicPem); + const privKey = new PrivateKey(privatePem); + const payload = Buffer.from([0xff, 0xfe, 0xfd, 0x00, 0x80]); + const encrypted = pubKey.encrypt(new Media(payload)); + const decrypted = privKey.decrypt(encrypted); + + expect(decrypted).toEqual(payload); + }); + it('should throw InvalidLengthError for oversized payloads', () => { const key = new PublicKey(publicPem); const payload = Buffer.alloc(1024 * 1024 + 1); diff --git a/tests/value-objects/hashes/MD5Hash.spec.ts b/tests/value-objects/hashes/MD5Hash.spec.ts index ed46088..01e5af2 100644 --- a/tests/value-objects/hashes/MD5Hash.spec.ts +++ b/tests/value-objects/hashes/MD5Hash.spec.ts @@ -2,6 +2,7 @@ import { createHash } from 'crypto'; import { InvalidHashError, + Media, MD5Hash, NullObject, StringValueObject, @@ -98,6 +99,13 @@ describe('MD5Hash', () => { expect(hash.valueOf()).toBe(expected); }); + it('should compute MD5 hash from raw Media bytes', () => { + const input = Buffer.from([0xff, 0xfe, 0xfd, 0x00, 0x80]); + const expected = computeMD5(input); + const hash = MD5Hash.from(new Media(input)); + expect(hash.valueOf()).toBe(expected); + }); + it('should compute MD5 hash from StringValueObject', () => { const input = new StringValueObject('hello'); const expected = computeMD5(input.valueOf()); diff --git a/tests/value-objects/hashes/SHA256Hash.spec.ts b/tests/value-objects/hashes/SHA256Hash.spec.ts index c33b3c9..054a2b1 100644 --- a/tests/value-objects/hashes/SHA256Hash.spec.ts +++ b/tests/value-objects/hashes/SHA256Hash.spec.ts @@ -3,6 +3,7 @@ import { SHA256Hash, NullObject, InvalidHashError, + Media, StringValueObject, } from '../../../src'; @@ -102,6 +103,13 @@ describe('SHA256Hash', () => { expect(hash.valueOf()).toBe(expected); }); + it('should compute SHA256 hash from raw Media bytes', () => { + const input = Buffer.from([0xff, 0xfe, 0xfd, 0x00, 0x80]); + const expected = computeSHA256(input); + const hash = SHA256Hash.from(new Media(input)); + expect(hash.valueOf()).toBe(expected); + }); + it('should compute SHA256 hash from StringValueObject', () => { const input = new StringValueObject('hello'); const expected = computeSHA256(input.valueOf()); diff --git a/tests/value-objects/hashes/SHA512Hash.spec.ts b/tests/value-objects/hashes/SHA512Hash.spec.ts index 458f00f..2139623 100644 --- a/tests/value-objects/hashes/SHA512Hash.spec.ts +++ b/tests/value-objects/hashes/SHA512Hash.spec.ts @@ -3,6 +3,7 @@ import { SHA512Hash, NullObject, InvalidHashError, + Media, StringValueObject, } from '../../../src'; @@ -102,6 +103,13 @@ describe('SHA512Hash', () => { expect(hash.valueOf()).toBe(expected); }); + it('should compute SHA512 hash from raw Media bytes', () => { + const input = Buffer.from([0xff, 0xfe, 0xfd, 0x00, 0x80]); + const expected = computeSHA512(input); + const hash = SHA512Hash.from(new Media(input)); + expect(hash.valueOf()).toBe(expected); + }); + it('should compute SHA512 hash from StringValueObject', () => { const input = new StringValueObject('hello'); const expected = computeSHA512(input.valueOf()); diff --git a/tests/value-objects/media/Media.spec.ts b/tests/value-objects/media/Media.spec.ts index 9338fc3..e7b81f3 100644 --- a/tests/value-objects/media/Media.spec.ts +++ b/tests/value-objects/media/Media.spec.ts @@ -1,6 +1,7 @@ import { NullObjectError } from '../../../src/errors/NullObjectError'; import { Media } from '../../../src/value-objects/media/Media'; import { NullObject } from '../../../src/value-objects/NullObject'; +import { UniqueObjectArray } from '../../../src/value-objects/UniqueObjectArray'; describe('Media', () => { const testContent = 'hello world'; @@ -23,6 +24,17 @@ describe('Media', () => { expect(media).toBeInstanceOf(Media); expect(media.valueOf()).toBe(testContent); }); + + it('should preserve non-text bytes from a Buffer', () => { + const source = Buffer.from([0xff, 0xfe, 0xfd, 0x00, 0x80]); + const media = new Media(source); + + source[0] = 0x00; + + expect(media.getBuffer()).toEqual( + Buffer.from([0xff, 0xfe, 0xfd, 0x00, 0x80]), + ); + }); }); describe('getBuffer', () => { @@ -51,6 +63,40 @@ describe('Media', () => { }); }); + describe('isEqual', () => { + it('should compare Media instances using preserved bytes', () => { + const media = new Media(Buffer.from([0xff])); + const other = new Media(Buffer.from([0xfe])); + + expect(media.toString()).toBe(other.toString()); + expect(media.getBuffer()).not.toEqual(other.getBuffer()); + expect(media.isEqual(other)).toBeFalse(); + }); + + it('should compare Media instances with the same bytes as equal', () => { + const media = new Media(Buffer.from([0xff])); + const other = new Media(Buffer.from([0xff])); + + expect(media.isEqual(other)).toBeTrue(); + }); + + it('should compare Media instances with buffers using preserved bytes', () => { + const media = new Media(Buffer.from([0xff])); + + expect(media.isEqual(Buffer.from([0xff]))).toBeTrue(); + expect(media.isEqual(Buffer.from([0xfe]))).toBeFalse(); + }); + + it('should keep distinct binary payloads in unique collections', () => { + const medias = UniqueObjectArray.fromArray([ + new Media(Buffer.from([0xff])), + new Media(Buffer.from([0xfe])), + ]); + + expect(medias.length()).toBe(2); + }); + }); + describe('NullObject behavior', () => { it('should throw NullObjectError when calling methods on a null media', () => { const nullMedia = new Media(undefined as unknown as string);