-
-
Notifications
You must be signed in to change notification settings - Fork 40
Added Nonce Enforcer as Caveat in Delegation Core #44
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| import { isHexString } from '@metamask/utils'; | ||
| import type { BytesLike } from '@metamask/utils'; | ||
|
|
||
| import { | ||
| defaultOptions, | ||
| prepareResult, | ||
| type EncodingOptions, | ||
| type ResultValue, | ||
| } from '../returns'; | ||
| import type { Hex } from '../types'; | ||
|
|
||
| // char length of 32 byte hex string (including 0x prefix) | ||
| const MAX_NONCE_STRING_LENGTH = 66; | ||
|
|
||
| /** | ||
| * Terms for configuring a Nonce caveat. | ||
| */ | ||
| export type NonceTerms = { | ||
| /** The nonce as a hex string to allow bulk revocation of delegations. */ | ||
| nonce: BytesLike; | ||
| }; | ||
|
|
||
| /** | ||
| * Creates terms for a Nonce caveat that uses a nonce value for bulk revocation of delegations. | ||
| * | ||
| * @param terms - The terms for the Nonce caveat. | ||
| * @param encodingOptions - The encoding options for the result. | ||
| * @returns The terms as a 32-byte hex string. | ||
| * @throws Error if the nonce is invalid. | ||
| */ | ||
| export function createNonceTerms( | ||
| terms: NonceTerms, | ||
| encodingOptions?: EncodingOptions<'hex'>, | ||
| ): Hex; | ||
| export function createNonceTerms( | ||
| terms: NonceTerms, | ||
| encodingOptions: EncodingOptions<'bytes'>, | ||
| ): Uint8Array; | ||
| /** | ||
| * Creates terms for a Nonce caveat that uses a nonce value for bulk revocation of delegations. | ||
| * | ||
| * @param terms - The terms for the Nonce caveat. | ||
| * @param encodingOptions - The encoding options for the result. | ||
| * @returns The terms as a 32-byte hex string. | ||
| * @throws Error if the nonce is invalid. | ||
| */ | ||
| export function createNonceTerms( | ||
| terms: NonceTerms, | ||
| encodingOptions: EncodingOptions<ResultValue> = defaultOptions, | ||
| ): Hex | Uint8Array { | ||
| const { nonce } = terms; | ||
|
|
||
| if (!nonce || nonce === '0x') { | ||
| throw new Error('Invalid nonce: must be a non-empty hex string'); | ||
| } | ||
|
|
||
| if (typeof nonce !== 'string' || !nonce.startsWith('0x')) { | ||
| throw new Error('Invalid nonce: must be a valid hex string'); | ||
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This needs to accept ArrayBuffer also, as that's included in the We could either:
Also, can we drop this check entirely, as line 61 does the same check, but also validates the "body" of the hex string?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added, please take a look |
||
|
|
||
| if (!isHexString(nonce)) { | ||
| throw new Error('Invalid nonce: must be a valid hex string'); | ||
| } | ||
|
|
||
| if (nonce.length > MAX_NONCE_STRING_LENGTH) { | ||
| throw new Error('Invalid nonce: must be 32 bytes or less in length'); | ||
| } | ||
|
|
||
| // Remove '0x' prefix for padding, then add it back | ||
| const nonceWithoutPrefix = nonce.slice(2); | ||
| const paddedNonce = nonceWithoutPrefix.padStart(64, '0'); // 64 hex chars = 32 bytes | ||
| const hexValue = `0x${paddedNonce}`; | ||
|
|
||
| return prepareResult(hexValue, encodingOptions); | ||
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we please add some tests for ArrayBuffer input ?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added, please take a look |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,204 @@ | ||
| import { describe, it, expect } from 'vitest'; | ||
|
|
||
| import { createNonceTerms } from '../../src/caveats/nonce'; | ||
|
|
||
| describe('createNonceTerms', () => { | ||
| const EXPECTED_BYTE_LENGTH = 32; // 32 bytes for nonce | ||
|
|
||
| it('creates valid terms for simple nonce', () => { | ||
| const nonce = '0x1234567890abcdef'; | ||
| const result = createNonceTerms({ nonce }); | ||
|
|
||
| expect(result).toStrictEqual( | ||
| '0x0000000000000000000000000000000000000000000000001234567890abcdef', | ||
| ); | ||
| }); | ||
|
|
||
| it('creates valid terms for zero nonce', () => { | ||
| const nonce = '0x0'; | ||
| const result = createNonceTerms({ nonce }); | ||
|
|
||
| expect(result).toStrictEqual( | ||
| '0x0000000000000000000000000000000000000000000000000000000000000000', | ||
| ); | ||
| }); | ||
|
|
||
| it('creates valid terms for minimal nonce', () => { | ||
| const nonce = '0x1'; | ||
| const result = createNonceTerms({ nonce }); | ||
|
|
||
| expect(result).toStrictEqual( | ||
| '0x0000000000000000000000000000000000000000000000000000000000000001', | ||
| ); | ||
| }); | ||
|
|
||
| it('creates valid terms for full 32-byte nonce', () => { | ||
| const nonce = | ||
| '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'; | ||
| const result = createNonceTerms({ nonce }); | ||
|
|
||
| expect(result).toStrictEqual(nonce); | ||
| }); | ||
|
|
||
| it('creates valid terms for uppercase hex nonce', () => { | ||
| const nonce = '0x1234567890ABCDEF'; | ||
| const result = createNonceTerms({ nonce }); | ||
|
|
||
| expect(result).toStrictEqual( | ||
| '0x0000000000000000000000000000000000000000000000001234567890ABCDEF', | ||
| ); | ||
| }); | ||
|
|
||
| it('creates valid terms for mixed case hex nonce', () => { | ||
| const nonce = '0x1234567890AbCdEf'; | ||
| const result = createNonceTerms({ nonce }); | ||
|
|
||
| expect(result).toStrictEqual( | ||
| '0x0000000000000000000000000000000000000000000000001234567890AbCdEf', | ||
| ); | ||
| }); | ||
|
|
||
| it('pads shorter hex values with leading zeros', () => { | ||
| const nonce = '0xff'; | ||
| const result = createNonceTerms({ nonce }); | ||
|
|
||
| expect(result).toStrictEqual( | ||
| '0x00000000000000000000000000000000000000000000000000000000000000ff', | ||
| ); | ||
| }); | ||
|
|
||
| it('throws an error for empty nonce', () => { | ||
| const nonce = '0x'; | ||
|
|
||
| expect(() => createNonceTerms({ nonce })).toThrow( | ||
| 'Invalid nonce: must be a non-empty hex string', | ||
| ); | ||
| }); | ||
|
|
||
| it('throws an error for undefined nonce', () => { | ||
| expect(() => createNonceTerms({ nonce: undefined as any })).toThrow( | ||
| 'Invalid nonce: must be a non-empty hex string', | ||
| ); | ||
| }); | ||
|
|
||
| it('throws an error for null nonce', () => { | ||
| expect(() => createNonceTerms({ nonce: null as any })).toThrow( | ||
| 'Invalid nonce: must be a non-empty hex string', | ||
| ); | ||
| }); | ||
|
|
||
| it('throws an error for nonce without 0x prefix', () => { | ||
| const nonce = '1234567890abcdef' as any; | ||
|
|
||
| expect(() => createNonceTerms({ nonce })).toThrow( | ||
| 'Invalid nonce: must be a valid hex string', | ||
| ); | ||
| }); | ||
|
|
||
| it('throws an error for invalid hex characters', () => { | ||
| const nonce = '0x1234567890abcdefg' as any; | ||
|
|
||
| expect(() => createNonceTerms({ nonce })).toThrow( | ||
| 'Invalid nonce: must be a valid hex string', | ||
| ); | ||
| }); | ||
|
|
||
| it('throws an error for non-string nonce', () => { | ||
| const nonce = 123456 as any; | ||
|
|
||
| expect(() => createNonceTerms({ nonce })).toThrow( | ||
| 'Invalid nonce: must be a valid hex string', | ||
| ); | ||
| }); | ||
|
|
||
| it('throws an error for nonce longer than 32 bytes', () => { | ||
| // 33 bytes (66 hex chars + 0x prefix = 68 chars total, which exceeds 66) | ||
| const nonce = | ||
| '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12' as any; | ||
|
|
||
| expect(() => createNonceTerms({ nonce })).toThrow( | ||
| 'Invalid nonce: must be 32 bytes or less in length', | ||
| ); | ||
| }); | ||
|
|
||
| it('accepts nonce with exactly 32 bytes', () => { | ||
| // 32 bytes (64 hex chars + 0x prefix = 66 chars total) | ||
| const nonce = | ||
| '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'; | ||
| const result = createNonceTerms({ nonce }); | ||
|
|
||
| expect(result).toStrictEqual(nonce); | ||
| }); | ||
|
|
||
| it('throws an error for string that looks like hex but has odd length', () => { | ||
| const nonce = '0x123' as any; | ||
| // This should still work as we pad it | ||
| const result = createNonceTerms({ nonce }); | ||
|
|
||
| expect(result).toStrictEqual( | ||
| '0x0000000000000000000000000000000000000000000000000000000000000123', | ||
| ); | ||
| }); | ||
|
|
||
| // Tests for bytes return type | ||
| describe('bytes return type', () => { | ||
| it('returns Uint8Array when bytes encoding is specified', () => { | ||
| const nonce = '0x1234567890abcdef'; | ||
| const result = createNonceTerms({ nonce }, { out: 'bytes' }); | ||
|
|
||
| expect(result).toBeInstanceOf(Uint8Array); | ||
| expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); | ||
| }); | ||
|
|
||
| it('returns Uint8Array for minimal nonce with bytes encoding', () => { | ||
| const nonce = '0x1'; | ||
| const result = createNonceTerms({ nonce }, { out: 'bytes' }); | ||
|
|
||
| expect(result).toBeInstanceOf(Uint8Array); | ||
| expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); | ||
| // Should be 31 zeros followed by 1 | ||
| const expectedBytes = new Array(EXPECTED_BYTE_LENGTH).fill(0); | ||
| expectedBytes[EXPECTED_BYTE_LENGTH - 1] = 1; | ||
| expect(Array.from(result)).toEqual(expectedBytes); | ||
| }); | ||
|
|
||
| it('returns Uint8Array for zero nonce with bytes encoding', () => { | ||
| const nonce = '0x0'; | ||
| const result = createNonceTerms({ nonce }, { out: 'bytes' }); | ||
|
|
||
| expect(result).toBeInstanceOf(Uint8Array); | ||
| expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); | ||
| // Should be all zeros | ||
| const expectedBytes = new Array(EXPECTED_BYTE_LENGTH).fill(0); | ||
| expect(Array.from(result)).toEqual(expectedBytes); | ||
| }); | ||
|
|
||
| it('returns Uint8Array for full nonce with bytes encoding', () => { | ||
| const nonce = | ||
| '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'; | ||
| const result = createNonceTerms({ nonce }, { out: 'bytes' }); | ||
|
|
||
| expect(result).toBeInstanceOf(Uint8Array); | ||
| expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); | ||
| // Convert expected hex to bytes for comparison | ||
| const expectedBytes = [ | ||
| 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, 0x12, 0x34, 0x56, 0x78, | ||
| 0x90, 0xab, 0xcd, 0xef, 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, | ||
| 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, | ||
| ]; | ||
| expect(Array.from(result)).toEqual(expectedBytes); | ||
| }); | ||
|
|
||
| it('returns Uint8Array for padded hex values with bytes encoding', () => { | ||
| const nonce = '0xff'; | ||
| const result = createNonceTerms({ nonce }, { out: 'bytes' }); | ||
|
|
||
| expect(result).toBeInstanceOf(Uint8Array); | ||
| expect(result).toHaveLength(EXPECTED_BYTE_LENGTH); | ||
| // Should be 31 zeros followed by 0xff | ||
| const expectedBytes = new Array(EXPECTED_BYTE_LENGTH).fill(0); | ||
| expectedBytes[EXPECTED_BYTE_LENGTH - 1] = 0xff; | ||
| expect(Array.from(result)).toEqual(expectedBytes); | ||
| }); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because this needs to accept ArrayBuffer, we should consider how we want to validate that - should we throw if the value is a zero-length ArrayBuffer, or should we consider that 0x00 - "0x" has a specific meaning (null) which I don't think zero-length ArrayBuffer does?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added, please take a look