Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions apps/backend/src/modules/escrow/utils/metadata-hash.util.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,24 @@ describe('normalizeMetadataHash', () => {
expect(normalizeMetadataHash(cid)).toBe(digestHex);
});

it('rejects all-zero raw hex digests', () => {
expect(() => normalizeMetadataHash('0'.repeat(64))).toThrow('zeroes');
});

it('rejects all-zero cid digests', () => {
const cid = `b${encodeBase32(
Uint8Array.from([0x01, 0x55, 0x12, 0x20, ...new Uint8Array(32)]),
)}`;

expect(() => normalizeMetadataHash(cid)).toThrow('zeroes');
});

it('rejects malformed raw hex digests', () => {
expect(() => normalizeMetadataHash(digestHex.slice(0, 62))).toThrow(
'64 hex characters',
);
});

it('rejects non-sha256 multihashes', () => {
const cid = `b${encodeBase32(
Uint8Array.from([0x01, 0x55, 0x13, 0x20, ...digest]),
Expand Down
18 changes: 16 additions & 2 deletions apps/backend/src/modules/escrow/utils/metadata-hash.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,27 @@ const BASE32_ALPHABET = 'abcdefghijklmnopqrstuvwxyz234567';
const BASE58BTC_ALPHABET =
'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
const HEX_32_RE = /^[0-9a-f]{64}$/;
const HEX_RE = /^[0-9a-f]+$/i;
const ZERO_DIGEST_HEX = '0'.repeat(SHA256_DIGEST_LENGTH * 2);

export function normalizeMetadataHash(reference: string): string {
const value = sanitizeReference(reference);
const lowered = value.toLowerCase();

if (HEX_32_RE.test(lowered)) {
return lowered;
return validateDigestHex(lowered);
}

if (HEX_RE.test(value)) {
throw new Error('metadata hash must be 64 hex characters');
}

const cid = extractCid(value);
const digest = cid.startsWith('Qm')
? extractDigestFromCidV0(cid)
: extractDigestFromCidV1(cid);

return bytesToHex(digest);
return validateDigestHex(bytesToHex(digest));
}

function sanitizeReference(reference: string): string {
Expand Down Expand Up @@ -199,3 +205,11 @@ function bytesToHex(bytes: Uint8Array): string {
'',
);
}

function validateDigestHex(digestHex: string): string {
if (digestHex === ZERO_DIGEST_HEX) {
throw new Error('metadata hash cannot be all zeroes');
}

return digestHex;
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe('EscrowOperationsService Integration', () => {
token,
milestones,
deadline,
'0'.repeat(64), // Valid hex metadata hash
'1'.repeat(64), // Valid non-zero hex metadata hash
);

const op = ops[0] as any as {
Expand Down
18 changes: 18 additions & 0 deletions apps/frontend/lib/metadata-hash.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,24 @@ describe('normalizeMetadataHash', () => {
expect(normalizeMetadataHash(cid)).toBe(digestHex);
});

it('rejects all-zero raw hex digests', () => {
expect(() => normalizeMetadataHash('0'.repeat(64))).toThrow('zeroes');
});

it('rejects all-zero cid digests', () => {
const cid = `b${encodeBase32(
Uint8Array.from([0x01, 0x55, 0x12, 0x20, ...new Uint8Array(32)]),
)}`;

expect(() => normalizeMetadataHash(cid)).toThrow('zeroes');
});

it('rejects malformed raw hex digests', () => {
expect(() => normalizeMetadataHash(digestHex.slice(0, 62))).toThrow(
'64 hex characters',
);
});

it('rejects non-sha256 multihashes', () => {
const cid = `b${encodeBase32(
Uint8Array.from([0x01, 0x55, 0x13, 0x20, ...digest]),
Expand Down
18 changes: 16 additions & 2 deletions apps/frontend/lib/metadata-hash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,27 @@ const BASE32_ALPHABET = 'abcdefghijklmnopqrstuvwxyz234567';
const BASE58BTC_ALPHABET =
'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
const HEX_32_RE = /^[0-9a-f]{64}$/;
const HEX_RE = /^[0-9a-f]+$/i;
const ZERO_DIGEST_HEX = '0'.repeat(SHA256_DIGEST_LENGTH * 2);

export function normalizeMetadataHash(reference: string): string {
const value = sanitizeReference(reference);
const lowered = value.toLowerCase();

if (HEX_32_RE.test(lowered)) {
return lowered;
return validateDigestHex(lowered);
}

if (HEX_RE.test(value)) {
throw new Error('metadata hash must be 64 hex characters');
}

const cid = extractCid(value);
const digest = cid.startsWith('Qm')
? extractDigestFromCidV0(cid)
: extractDigestFromCidV1(cid);

return bytesToHex(digest);
return validateDigestHex(bytesToHex(digest));
}

function sanitizeReference(reference: string): string {
Expand Down Expand Up @@ -192,3 +198,11 @@ function bytesToHex(bytes: Uint8Array): string {
'',
);
}

function validateDigestHex(digestHex: string): string {
if (digestHex === ZERO_DIGEST_HEX) {
throw new Error('metadata hash cannot be all zeroes');
}

return digestHex;
}
2 changes: 1 addition & 1 deletion apps/onchain/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ pub struct Escrow {
pub threshold_amount: i128, // Threshold amount for multi-sig requirement
pub required_signatures: u32, // Number of signatures required for release
pub collected_signatures: Vec<Address>, // Addresses that have signed for release
pub metadata_hash: BytesN<32>, // IPFS metadata hash for the escrow agreement
pub metadata_hash: BytesN<32>, // Raw SHA-256 digest bytes for escrow metadata
}

#[contracttype]
Expand Down
Loading