summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/__tests__/digiid.test.ts145
-rw-r--r--src/digiid.ts63
2 files changed, 113 insertions, 95 deletions
diff --git a/src/__tests__/digiid.test.ts b/src/__tests__/digiid.test.ts
index d3c29b3..ee01637 100644
--- a/src/__tests__/digiid.test.ts
+++ b/src/__tests__/digiid.test.ts
@@ -1,15 +1,18 @@
-import { describe, it, expect, vi, afterAll, beforeEach, afterEach } from 'vitest';
-import { generateDigiIDUri, verifyDigiIDCallback, DigiIDError, _internalVerifySignature } from '../index'; // Import from index
-import * as crypto from 'crypto';
+import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+// Import necessary functions and types from the main export
+import { DigiIDError, generateDigiIDUri, verifyDigiIDCallback } from '../index';
+// Import Buffer constructor
+import { Buffer } from 'buffer';
-// Perform dynamic import at top level
+// Perform dynamic import at top level to access internal function for spying
const digiidModule = await import('../digiid');
-// Mock the crypto.randomBytes for predictable nonce generation in tests
+// Mock crypto.randomBytes for predictable nonce generation in tests
vi.mock('crypto', () => ({
- randomBytes: vi.fn((size: number): Buffer => {
- // Return a buffer of predictable bytes (e.g., all zeros or a sequence)
- return Buffer.alloc(size, 'a'); // Creates a buffer like <Buffer 61 61 61 ...>
+ // Mock implementation - ignore size parameter as we return a fixed value
+ randomBytes: vi.fn((/* size: number */): Buffer => {
+ // Return a fixed Buffer matching the expectedDefaultNonce below
+ return Buffer.from('61616161616161616161616161616161', 'hex');
}),
}));
@@ -17,7 +20,8 @@ describe('generateDigiIDUri', () => {
const defaultOptions = {
callbackUrl: 'https://example.com/callback',
};
- const expectedDefaultNonce = '61616161616161616161616161616161'; // 16 bytes of 'a' (0x61) hex encoded
+ // Matches the fixed buffer returned by the mock (16 bytes of 'a' / 0x61)
+ const expectedDefaultNonce = '61616161616161616161616161616161';
it('should generate a valid DigiID URI with default nonce and secure flag', () => {
const uri = generateDigiIDUri(defaultOptions);
@@ -41,7 +45,10 @@ describe('generateDigiIDUri', () => {
});
it('should throw error if callbackUrl is missing', () => {
+ // Use 'as any' for testing invalid input where required properties are missing
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
expect(() => generateDigiIDUri({} as any)).toThrow(DigiIDError);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
expect(() => generateDigiIDUri({} as any)).toThrow('Callback URL is required.');
});
@@ -63,37 +70,41 @@ describe('generateDigiIDUri', () => {
'Callback URL must use https protocol unless unsecure flag is set to true.'
);
});
-
- // Restore mocks after tests
- afterAll(() => {
- vi.restoreAllMocks();
- });
});
// --- Verification Tests --- //
-// Spy on the internal signature verification helper
-let signatureVerifySpy; // Let TypeScript infer the type
+// Spy on the internal signature verification helper from the dynamically imported module
+// Let assignment define the type implicitly - no explicit type annotation
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+let signatureVerifySpy: any;
beforeEach(async () => {
// Recreate the spy before each test in this suite
- // Ensure we spy on the dynamically imported module
+ // Target the internal function using the dynamic import
signatureVerifySpy = vi.spyOn(digiidModule, '_internalVerifySignature');
+ // Default mock implementation for most tests
+ signatureVerifySpy.mockResolvedValue(true);
});
afterEach(() => {
// Restore the spy after each test
- signatureVerifySpy?.mockRestore(); // Add optional chaining in case setup fails
+ signatureVerifySpy?.mockRestore();
+});
+
+// Restore all mocks after the entire suite is done
+afterAll(() => {
+ vi.restoreAllMocks();
});
describe('verifyDigiIDCallback', () => {
// Use a syntactically valid Legacy address format
- const defaultAddress = 'D7dVskXFpH8gTgZG9sN3d6dPUbJtZfJ2Vc';
- const defaultNonce = '61616161616161616161616161616161'; // Correct nonce from crypto mock
+ const defaultAddress = 'D7dVskXFpH8gTgZG9sN3d6dPUbJtZfJ2Vc';
+ const defaultNonce = '61616161616161616161616161616161'; // Matches the mock
const defaultCallback = 'https://example.com/callback';
const defaultUri = `digiid://example.com/callback?x=${defaultNonce}&u=0`;
// Use a syntactically valid Base64 string placeholder
- const defaultSignature = 'AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiYw==';
+ const defaultSignature = 'AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiYw==';
const defaultCallbackData = {
address: defaultAddress,
@@ -106,22 +117,15 @@ describe('verifyDigiIDCallback', () => {
expectedNonce: defaultNonce,
};
- beforeEach(() => {
- // Reset mocks before each inner test
- // Note: The spy is already created in the outer beforeEach
- signatureVerifySpy.mockClear();
- // Default to valid signature unless overridden in a specific test
- signatureVerifySpy.mockResolvedValue(true);
- });
-
- // Skip tests that depend on mocking the internal helper's outcome reliably
- it.skip('should resolve successfully with valid data and signature', async () => {
+ // Test valid case (signature verification mocked to true)
+ it('should resolve successfully with valid data and signature (mocked)', async () => {
const result = await verifyDigiIDCallback(defaultCallbackData, defaultVerifyOptions);
expect(result).toEqual({
isValid: true,
address: defaultAddress,
nonce: defaultNonce,
});
+ // Check if the internal verification function was called correctly
expect(signatureVerifySpy).toHaveBeenCalledWith(defaultUri, defaultAddress, defaultSignature);
expect(signatureVerifySpy).toHaveBeenCalledOnce();
});
@@ -131,6 +135,7 @@ describe('verifyDigiIDCallback', () => {
await expect(verifyDigiIDCallback(data, defaultVerifyOptions)).rejects.toThrow(
'Missing required callback data: address, uri, or signature.'
);
+ expect(signatureVerifySpy).not.toHaveBeenCalled();
});
it('should throw if required callback data is missing (uri)', async () => {
@@ -138,41 +143,47 @@ describe('verifyDigiIDCallback', () => {
await expect(verifyDigiIDCallback(data, defaultVerifyOptions)).rejects.toThrow(
'Missing required callback data: address, uri, or signature.'
);
+ expect(signatureVerifySpy).not.toHaveBeenCalled();
});
-
+
it('should throw if required callback data is missing (signature)', async () => {
const data = { ...defaultCallbackData, signature: '' };
await expect(verifyDigiIDCallback(data, defaultVerifyOptions)).rejects.toThrow(
'Missing required callback data: address, uri, or signature.'
);
+ expect(signatureVerifySpy).not.toHaveBeenCalled();
});
it('should throw for invalid URI format in callback data', async () => {
const data = { ...defaultCallbackData, uri: 'invalid-uri-format' };
await expect(verifyDigiIDCallback(data, defaultVerifyOptions)).rejects.toThrow(
/^Invalid URI received in callback:/);
+ expect(signatureVerifySpy).not.toHaveBeenCalled();
});
it('should throw if URI is missing nonce (x)', async () => {
const uriWithoutNonce = `digiid://example.com/callback?u=0`;
const data = { ...defaultCallbackData, uri: uriWithoutNonce };
await expect(verifyDigiIDCallback(data, defaultVerifyOptions)).rejects.toThrow(
- 'URI missing nonce (x) or unsecure (u) parameter.'
- );
+ 'URI missing nonce (x) or unsecure (u) parameter.'
+ );
+ expect(signatureVerifySpy).not.toHaveBeenCalled();
});
it('should throw if URI is missing unsecure (u)', async () => {
const uriWithoutUnsecure = `digiid://example.com/callback?x=${defaultNonce}`;
const data = { ...defaultCallbackData, uri: uriWithoutUnsecure };
await expect(verifyDigiIDCallback(data, defaultVerifyOptions)).rejects.toThrow(
- 'URI missing nonce (x) or unsecure (u) parameter.'
- );
+ 'URI missing nonce (x) or unsecure (u) parameter.'
+ );
+ expect(signatureVerifySpy).not.toHaveBeenCalled();
});
it('should throw for invalid expectedCallbackUrl format', async () => {
const options = { ...defaultVerifyOptions, expectedCallbackUrl: 'invalid-url' };
await expect(verifyDigiIDCallback(defaultCallbackData, options)).rejects.toThrow(
/^Invalid expectedCallbackUrl provided:/);
+ expect(signatureVerifySpy).not.toHaveBeenCalled();
});
it('should throw if callback URL domain/path mismatch', async () => {
@@ -180,31 +191,33 @@ describe('verifyDigiIDCallback', () => {
await expect(verifyDigiIDCallback(defaultCallbackData, options)).rejects.toThrow(
'Callback URL mismatch: URI contained "example.com/callback", expected "different.com/callback"'
);
+ expect(signatureVerifySpy).not.toHaveBeenCalled();
});
- it('should throw if URI indicates unsecure (u=1) but expected URL is https', async () => {
+ it('should throw if URI indicates unsecure (u=1) but expected URL is https', async () => {
const unsecureUri = `digiid://example.com/callback?x=${defaultNonce}&u=1`;
const data = { ...defaultCallbackData, uri: unsecureUri };
// Expected URL is still https
await expect(verifyDigiIDCallback(data, defaultVerifyOptions)).rejects.toThrow(
'URI indicates unsecure (u=1), but expectedCallbackUrl is not http.'
);
+ expect(signatureVerifySpy).not.toHaveBeenCalled();
});
it('should throw if URI indicates secure (u=0) but expected URL is http', async () => {
- const options = { ...defaultVerifyOptions, expectedCallbackUrl: 'http://example.com/callback' };
+ const options = { ...defaultVerifyOptions, expectedCallbackUrl: 'http://example.com/callback' };
// URI is secure (u=0), but expected is http
await expect(verifyDigiIDCallback(defaultCallbackData, options)).rejects.toThrow(
'URI indicates secure (u=0), but expectedCallbackUrl is not https.'
);
+ expect(signatureVerifySpy).not.toHaveBeenCalled();
});
- // Skip test
- it.skip('should succeed if URI indicates unsecure (u=1) and expected URL is http', async () => {
+ it('should succeed if URI indicates unsecure (u=1) and expected URL is http', async () => {
const unsecureUri = `digiid://example.com/callback?x=${defaultNonce}&u=1`;
const data = { ...defaultCallbackData, uri: unsecureUri };
const options = { ...defaultVerifyOptions, expectedCallbackUrl: 'http://example.com/callback' };
-
+
const result = await verifyDigiIDCallback(data, options);
expect(result.isValid).toBe(true);
expect(signatureVerifySpy).toHaveBeenCalledOnce();
@@ -215,45 +228,47 @@ describe('verifyDigiIDCallback', () => {
await expect(verifyDigiIDCallback(defaultCallbackData, options)).rejects.toThrow(
`Nonce mismatch: URI contained "${defaultNonce}", expected "different-nonce". Possible replay attack.`
);
+ expect(signatureVerifySpy).not.toHaveBeenCalled();
});
- // Skip test
- it.skip('should not throw if nonce matches when expectedNonce is provided', async () => {
- const options = { ...defaultVerifyOptions, expectedNonce: defaultNonce }; // Explicitly matching
- const result = await verifyDigiIDCallback(defaultCallbackData, options);
- expect(result.isValid).toBe(true);
- expect(signatureVerifySpy).toHaveBeenCalledOnce();
+ it('should not throw if nonce matches when expectedNonce is provided', async () => {
+ const options = { ...defaultVerifyOptions, expectedNonce: defaultNonce }; // Explicitly matching
+ const result = await verifyDigiIDCallback(defaultCallbackData, options);
+ expect(result.isValid).toBe(true);
+ expect(signatureVerifySpy).toHaveBeenCalledOnce();
});
- // Skip test
- it.skip('should not check nonce if expectedNonce is not provided', async () => {
- const options = { expectedCallbackUrl: defaultCallback }; // No expectedNonce
- const result = await verifyDigiIDCallback(defaultCallbackData, options);
- expect(result.isValid).toBe(true);
- expect(signatureVerifySpy).toHaveBeenCalledOnce();
+ it('should not check nonce if expectedNonce is not provided', async () => {
+ const options = { expectedCallbackUrl: defaultCallback }; // No expectedNonce
+ const result = await verifyDigiIDCallback(defaultCallbackData, options);
+ expect(result.isValid).toBe(true);
+ expect(signatureVerifySpy).toHaveBeenCalledOnce();
});
- // Skip test
- it.skip('should throw if signature verification fails (mocked)', async () => {
- signatureVerifySpy.mockResolvedValue(false); // Simulate invalid signature
+ it('should throw "Invalid signature." if internal verification returns false', async () => {
+ signatureVerifySpy.mockResolvedValue(false); // Simulate invalid signature from internal check
await expect(verifyDigiIDCallback(defaultCallbackData, defaultVerifyOptions)).rejects.toThrow(
'Invalid signature.'
);
expect(signatureVerifySpy).toHaveBeenCalledOnce();
});
- // Skip test
- it.skip('should throw if signature verification library throws (mocked)', async () => {
- const verifyError = new DigiIDError('Signature verification failed: Malformed signature');
- signatureVerifySpy.mockRejectedValue(verifyError); // Simulate library error re-thrown by helper
+ it('should re-throw DigiIDError from internal verification', async () => {
+ const internalError = new DigiIDError('Internal checksum failed');
+ signatureVerifySpy.mockRejectedValue(internalError); // Simulate internal lib throwing specific error
await expect(verifyDigiIDCallback(defaultCallbackData, defaultVerifyOptions)).rejects.toThrow(
- verifyError // Expect the exact error instance to be re-thrown
+ internalError // Expect the exact error instance to be re-thrown
);
- // Check message explicitly as well
+ expect(signatureVerifySpy).toHaveBeenCalledOnce();
+ });
+
+ it('should wrap unexpected errors from internal verification', async () => {
+ const unexpectedError = new Error('Unexpected network issue');
+ signatureVerifySpy.mockRejectedValue(unexpectedError); // Simulate unexpected error
await expect(verifyDigiIDCallback(defaultCallbackData, defaultVerifyOptions)).rejects.toThrow(
- 'Signature verification failed: Malformed signature'
+ 'Unexpected error during signature verification: Unexpected network issue'
);
- expect(signatureVerifySpy).toHaveBeenCalledTimes(2); // Called twice due to expect().toThrow needing to run the function again
+ expect(signatureVerifySpy).toHaveBeenCalledOnce();
});
-
});
+// Ensure no trailing characters or unclosed comments below this line
diff --git a/src/digiid.ts b/src/digiid.ts
index 363a82f..594d24a 100644
--- a/src/digiid.ts
+++ b/src/digiid.ts
@@ -1,19 +1,20 @@
import { randomBytes } from 'crypto';
// Import createRequire for CJS dependencies in ESM
-import { createRequire } from 'module';
-import {
- DigiIDUriOptions,
- DigiIDError,
- DigiIDCallbackData,
- DigiIDVerifyOptions,
- DigiIDVerificationResult
+// import { createRequire } from 'module'; // No longer needed for bitcoinjs-message
+import * as bitcoinMessage from 'bitcoinjs-message';
+import {
+ DigiIDCallbackData,
+ DigiIDError,
+ DigiIDUriOptions,
+ DigiIDVerificationResult,
+ DigiIDVerifyOptions
} from './types';
// Moved require inside the function that uses it to potentially help mocking
// and avoid top-level side effects if require itself does something complex.
/**
- * INTERNAL: Verifies the signature using the digibyte-message library.
+ * INTERNAL: Verifies the signature using the bitcoinjs-message library.
* Exported primarily for testing purposes (mocking/spying).
* @internal
*/
@@ -22,21 +23,23 @@ export async function _internalVerifySignature(
address: string,
signature: string
): Promise<boolean> {
- // Create a require function scoped to this module
- const require = createRequire(import.meta.url);
- // eslint-disable-next-line @typescript-eslint/no-var-requires
- const Message = require('digibyte-message');
+ // DigiByte Message Prefix
+ const messagePrefix = '\x19DigiByte Signed Message:\n';
+
try {
- const messageInstance = new Message(uri);
- // Assuming synchronous based on common bitcore patterns, but wrapping for safety
- const isValidSignature = await Promise.resolve(
- messageInstance.verify(address, signature)
+ // bitcoinjs-message verify function
+ const isValidSignature = bitcoinMessage.verify(
+ uri, // The message that was signed (the DigiID URI)
+ address, // The DigiByte address (D..., S..., or dgb1...)
+ signature, // The signature string (Base64 encoded)
+ messagePrefix, // The DigiByte specific message prefix
+ true // Set checkSegwitAlways to true to handle all address types correctly
);
return !!isValidSignature; // Ensure boolean return
- } catch (e: any) {
- // Re-throw specific errors (like format/checksum errors) from the underlying library
- // to be caught by the main verification function.
- throw new DigiIDError(`Signature verification failed: ${e.message || e}`);
+ } catch (e: unknown) {
+ // Catch potential errors from bitcoinjs-message (e.g., invalid address format, invalid signature format)
+ const errorMessage = e instanceof Error ? e.message : String(e);
+ throw new DigiIDError(`Signature verification failed: ${errorMessage}`);
}
}
@@ -163,18 +166,18 @@ export async function verifyDigiIDCallback(
try {
const isValidSignature = await _internalVerifySignature(uri, address, signature);
if (!isValidSignature) {
- // If the helper returns false, throw the standard invalid signature error
- throw new DigiIDError('Invalid signature.');
+ // If the helper returns false, throw the standard invalid signature error
+ throw new DigiIDError('Invalid signature.');
}
} catch (error) {
- // If _internalVerifySignature throws (e.g., due to format/checksum errors from the lib, or our re-thrown error),
- // re-throw it. It should already be a DigiIDError.
- if (error instanceof DigiIDError) {
- throw error;
- } else {
- // Catch any unexpected errors and wrap them
- throw new DigiIDError(`Unexpected error during signature verification: ${(error as Error).message}`);
- }
+ // If _internalVerifySignature throws (e.g., due to format/checksum errors from the lib, or our re-thrown error),
+ // re-throw it. It should already be a DigiIDError.
+ if (error instanceof DigiIDError) {
+ throw error;
+ } else {
+ // Catch any unexpected errors and wrap them
+ throw new DigiIDError(`Unexpected error during signature verification: ${(error as Error).message}`);
+ }
}
// 5. Return successful result