๐ฏ Learning Path:
Welcome to the REL-ID Data Signing codelab! This tutorial builds upon your existing MFA implementation to add secure cryptographic data signing capabilities using REL-ID SDK's authentication and signing infrastructure.
In this codelab, you'll enhance your existing MFA application with:
By completing this codelab, you'll master:
authenticateUserAndSignData() with proper parameter handlingonAuthenticateUserAndSignData callbacks and state updatesBefore starting this codelab, ensure you have:
The code to get started can be found in a GitHub repository.
You can clone the repository using the following command:
git clone https://github.com/uniken-public/codelab-cordova.git
Navigate to the relid-data-signing folder in the repository you cloned earlier
This codelab extends your MFA application with comprehensive data signing functionality:
Before implementing data signing functionality, let's understand the cryptographic concepts and security architecture that powers REL-ID's data signing capabilities.
Data signing is a cryptographic process that creates a digital signature for a piece of data, providing:
REL-ID's data signing implementation follows enterprise security standards:
User Data Input โ Authentication Challenge โ LDA/Password Verification โ
Cryptographic Signing โ Signed Payload โ Verification
Data signing is ideal for:
Key security guidelines:
Let's explore the three core APIs that power REL-ID's data signing functionality, understanding their parameters, responses, and integration patterns.
This is the primary API for initiating cryptographic data signing with user authentication.
com.uniken.rdnaplugin.RdnaClient.authenticateUserAndSignData(
successCallback,
errorCallback,
[payload, authLevel, authenticatorType, reason]
)
Parameter | Type | Required | Description |
| string | โ | The data to be cryptographically signed (max 500 characters) |
| number | โ | Authentication security level (0, 1, or 4 only) |
| number | โ | Type of authentication method (0 or 1 only) |
| string | โ | Human-readable reason for signing (max 100 characters) |
RDNAAuthLevel | RDNAAuthenticatorType | Supported Authentication | Description |
|
| No Authentication | No authentication required - NOT RECOMMENDED for production |
|
| Device biometric, Device passcode, or Password | Priority: Device biometric โ Device passcode โ Password |
| NOT SUPPORTED | โ SDK will error out | Level 2 is not supported for data signing |
| NOT SUPPORTED | โ SDK will error out | Level 3 is not supported for data signing |
|
| IDV Server Biometric | Maximum security - Any other authenticator type will cause SDK error |
REL-ID data signing supports three authentication modes:
authLevel: 0, // NONE
authenticatorType: 0 // NONE
authLevel: 1, // RDNA_AUTH_LEVEL_1
authenticatorType: 0 // NONE
authLevel: 4, // RDNA_AUTH_LEVEL_4
authenticatorType: 1 // RDNA_IDV_SERVER_BIOMETRIC
RDNA_IDV_SERVER_BIOMETRIC - other types will cause errors// Call signature (callbacks first, parameters in array)
com.uniken.rdnaplugin.RdnaClient.authenticateUserAndSignData(
(response) => {
// Success callback - response is JSON string
const result = JSON.parse(response);
console.log('Signing initiated successfully');
},
(error) => {
// Error callback - error is JSON string
const result = JSON.parse(error);
console.error('Signing failed:', result.error);
},
[payload, authLevel, authenticatorType, reason] // Parameters as array
);
// Service layer implementation (from www/src/uniken/services/rdnaService.js)
/**
* Authenticates user and signs data payload
* @param {string} payload - Data to sign (max 500 chars)
* @param {number} authLevel - Authentication level (0, 1, or 4)
* @param {number} authenticatorType - Authenticator type (0 or 1)
* @param {string} reason - Signing reason (max 100 chars)
* @returns {Promise} Resolves with sync response
*/
async authenticateUserAndSignData(payload, authLevel, authenticatorType, reason) {
return new Promise((resolve, reject) => {
console.log('RdnaService - Initiating data signing:', {
payloadLength: payload.length,
authLevel,
authenticatorType,
reason,
});
com.uniken.rdnaplugin.RdnaClient.authenticateUserAndSignData(
(response) => {
// Success callback - parse JSON string
const result = JSON.parse(response);
console.log('RdnaService - AuthenticateUserAndSignData sync response success');
if (result.error && result.error.longErrorCode === 0) {
resolve(result);
} else {
reject(result);
}
},
(error) => {
// Error callback - parse JSON string
const result = JSON.parse(error);
console.error('RdnaService - AuthenticateUserAndSignData sync response error:', result);
reject(result);
},
[payload, authLevel, authenticatorType, reason]
);
});
}
This API cleans up authentication state after signing completion or cancellation.
com.uniken.rdnaplugin.RdnaClient.resetAuthenticateUserAndSignDataState(
successCallback,
errorCallback
)
// Service layer cleanup implementation (from www/src/uniken/services/rdnaService.js)
/**
* Resets the data signing authentication state
* @returns {Promise} Resolves when state is reset
*/
async resetAuthenticateUserAndSignDataState() {
return new Promise((resolve, reject) => {
console.log('RdnaService - Resetting data signing authentication state');
com.uniken.rdnaplugin.RdnaClient.resetAuthenticateUserAndSignDataState(
(response) => {
const result = JSON.parse(response);
console.log('RdnaService - ResetAuthenticateUserAndSignDataState sync response success');
if (result.error && result.error.longErrorCode === 0) {
resolve(result);
} else {
reject(result);
}
},
(error) => {
const result = JSON.parse(error);
console.error('RdnaService - ResetAuthenticateUserAndSignDataState sync response error:', result);
reject(result);
}
);
});
}
This callback event delivers the final signing results after authentication completion.
/**
* @typedef {Object} DataSigningResponse
* @property {string} dataPayload - Original payload that was signed
* @property {number} dataPayloadLength - Length of the payload
* @property {string} reason - Reason provided for signing
* @property {string} payloadSignature - Cryptographic signature (base64)
* @property {string} dataSignatureID - Unique signature identifier
* @property {number} authLevel - Authentication level used
* @property {number} authenticationType - Authentication type used
* @property {Object} status - Operation status
* @property {Object} error - Error details (if any)
*/
// From www/src/uniken/services/rdnaEventManager.js
/**
* Handles onAuthenticateUserAndSignData event
* @param {Event} event - DOM event with signing response
*/
onAuthenticateUserAndSignData(event) {
console.log('RdnaEventManager - onAuthenticateUserAndSignData event received');
try {
// Parse event.response (JSON string from plugin)
const response = JSON.parse(event.response);
console.log('Data signing response:', JSON.stringify({
statusCode: response.status?.statusCode,
errorCode: response.error?.longErrorCode,
signatureLength: response.payloadSignature?.length
}, null, 2));
// Call registered handler
if (this.dataSigningResponseHandler) {
this.dataSigningResponseHandler(response);
}
} catch (error) {
console.error('RdnaEventManager - Failed to parse onAuthenticateUserAndSignData:', error);
}
}
// Handler registration
setDataSigningResponseHandler(callback) {
this.dataSigningResponseHandler = callback;
}
Success Indicators:
error.shortErrorCode === 0status.statusCode === 100payloadSignature and dataSignatureIDError Handling:
Now let's implement the service layer architecture that provides clean abstraction over REL-ID SDK data signing APIs. This follows established Cordova patterns with singleton services and JSDoc types.
First, let's examine the core SDK service implementation. This should already exist in your codelab from MFA implementation:
// www/src/uniken/services/rdnaService.js
/**
* Authenticates user and signs data payload
*
* This method initiates the data signing flow with step-up authentication.
* It requires user authentication and cryptographically signs the provided
* payload upon successful authentication.
*
* @param {string} payload - Data to sign (max 500 chars)
* @param {number} authLevel - Authentication level (0, 1, or 4)
* @param {number} authenticatorType - Authenticator type (0 or 1)
* @param {string} reason - Signing reason (max 100 chars)
* @returns {Promise<Object>} Sync response with error details
*/
async authenticateUserAndSignData(payload, authLevel, authenticatorType, reason) {
return new Promise((resolve, reject) => {
console.log('RdnaService - Initiating data signing:', {
payloadLength: payload.length,
authLevel,
authenticatorType,
reason,
});
com.uniken.rdnaplugin.RdnaClient.authenticateUserAndSignData(
(response) => {
console.log('RdnaService - AuthenticateUserAndSignData sync callback received');
const result = JSON.parse(response);
if (result.error && result.error.longErrorCode === 0) {
console.log('RdnaService - AuthenticateUserAndSignData sync response success');
resolve(result);
} else {
console.error('RdnaService - AuthenticateUserAndSignData sync response error:', result);
reject(result);
}
},
(error) => {
const result = JSON.parse(error);
console.error('RdnaService - AuthenticateUserAndSignData error callback:', result);
reject(result);
},
[payload, authLevel, authenticatorType, reason]
);
});
}
/**
* Resets the data signing authentication state
* @returns {Promise<Object>} Sync response
*/
async resetAuthenticateUserAndSignDataState() {
return new Promise((resolve, reject) => {
console.log('RdnaService - Resetting data signing authentication state');
com.uniken.rdnaplugin.RdnaClient.resetAuthenticateUserAndSignDataState(
(response) => {
console.log('RdnaService - ResetAuthenticateUserAndSignDataState sync callback received');
const result = JSON.parse(response);
if (result.error && result.error.longErrorCode === 0) {
console.log('RdnaService - ResetAuthenticateUserAndSignDataState sync response success');
resolve(result);
} else {
console.error('RdnaService - ResetAuthenticateUserAndSignDataState sync response error:', result);
reject(result);
}
},
(error) => {
const result = JSON.parse(error);
console.error('RdnaService - ResetAuthenticateUserAndSignDataState error callback:', result);
reject(result);
}
);
});
}
Create a high-level service that combines multiple concerns with user-friendly methods:
// www/src/tutorial/screens/dataSigning/DataSigningService.js
/**
* High-level service for data signing operations
* Combines rdnaService and DropdownDataService for complete functionality
*/
class DataSigningServiceClass {
/**
* Get dropdown data service instance
*/
get dropdownService() {
return window.DropdownDataService;
}
/**
* Get RDNA service instance
*/
get rdnaService() {
return rdnaService;
}
/**
* Initiates data signing with enum conversion
*
* @param {string} payload - Data to sign (max 500 chars)
* @param {string} authLevelDisplay - Display value (e.g., "RDNA_AUTH_LEVEL_4 (4)")
* @param {string} authenticatorTypeDisplay - Display value
* @param {string} reason - Signing reason (max 100 chars)
* @returns {Promise<Object>} Sync response
*/
async signData(payload, authLevelDisplay, authenticatorTypeDisplay, reason) {
console.log('DataSigningService - Starting data signing process');
try {
// Convert display values to numeric enums
const authLevel = this.dropdownService.convertAuthLevelToNumber(authLevelDisplay);
const authenticatorType = this.dropdownService.convertAuthenticatorTypeToNumber(authenticatorTypeDisplay);
console.log('DataSigningService - Converted enums:', JSON.stringify({
authLevel,
authenticatorType
}, null, 2));
// Call SDK API
const response = await rdnaService.authenticateUserAndSignData(
payload,
authLevel,
authenticatorType,
reason
);
console.log('DataSigningService - Data signing initiated successfully');
return response;
} catch (error) {
console.error('DataSigningService - Data signing failed:', error);
throw error;
}
}
/**
* Submit password for step-up authentication
* @param {string} password - User password
* @param {number} challengeMode - Challenge mode from getPassword event
* @returns {Promise<Object>} Sync response
*/
async submitPassword(password, challengeMode) {
console.log('DataSigningService - Submitting password for data signing');
try {
const response = await rdnaService.setPassword(password, challengeMode);
console.log('DataSigningService - Password submitted successfully');
return response;
} catch (error) {
console.error('DataSigningService - Password submission failed:', error);
throw error;
}
}
/**
* Reset data signing state (cleanup)
* @returns {Promise<void>}
*/
async resetState() {
console.log('DataSigningService - Resetting data signing state');
try {
await rdnaService.resetAuthenticateUserAndSignDataState();
console.log('DataSigningService - State reset successfully');
} catch (error) {
console.error('DataSigningService - State reset failed:', error);
// Don't throw - cleanup should not fail
}
}
/**
* Validate signing form input
* @param {string} payload
* @param {string} authLevel
* @param {string} authenticatorType
* @param {string} reason
* @returns {{isValid: boolean, errors: string[]}}
*/
validateSigningInput(payload, authLevel, authenticatorType, reason) {
const errors = [];
// Validate payload
if (!payload || payload.trim().length === 0) {
errors.push('Payload is required');
} else if (payload.length > 500) {
errors.push('Payload must be less than 500 characters');
}
// Validate auth level
if (!authLevel || authLevel === '') {
errors.push('Please select an authentication level');
} else if (!this.dropdownService.isValidAuthLevel(authLevel)) {
errors.push('Invalid authentication level');
}
// Validate authenticator type
if (!authenticatorType || authenticatorType === '') {
errors.push('Please select an authenticator type');
} else if (!this.dropdownService.isValidAuthenticatorType(authenticatorType)) {
errors.push('Invalid authenticator type');
}
// Validate reason
if (!reason || reason.trim().length === 0) {
errors.push('Reason is required');
} else if (reason.length > 100) {
errors.push('Reason must be less than 100 characters');
}
return {
isValid: errors.length === 0,
errors
};
}
/**
* Format signing response for display
* @param {Object} response - Raw signing response
* @returns {Object} Display-formatted response
*/
formatSigningResultForDisplay(response) {
return {
authLevel: response.authLevel?.toString() || 'N/A',
authenticationType: response.authenticationType?.toString() || 'N/A',
dataPayloadLength: response.dataPayloadLength?.toString() || 'N/A',
dataPayload: response.dataPayload || 'N/A',
payloadSignature: response.payloadSignature || 'N/A',
dataSignatureID: response.dataSignatureID || 'N/A',
reason: response.reason || 'N/A'
};
}
/**
* Convert to result info items array
* @param {Object} displayData
* @returns {Array<{name: string, value: string}>}
*/
convertToResultInfoItems(displayData) {
return [
{ name: 'Payload Signature', value: displayData.payloadSignature },
{ name: 'Data Signature ID', value: displayData.dataSignatureID },
{ name: 'Reason', value: displayData.reason },
{ name: 'Data Payload', value: displayData.dataPayload },
{ name: 'Auth Level', value: displayData.authLevel },
{ name: 'Authentication Type', value: displayData.authenticationType },
{ name: 'Data Payload Length', value: displayData.dataPayloadLength }
];
}
/**
* Get user-friendly error message for error codes
* @param {number} errorCode
* @returns {string}
*/
getErrorMessage(errorCode) {
switch (errorCode) {
case 0:
return 'Success';
case 214:
return 'Authentication method not supported. Please try a different authentication type.';
case 102:
return 'Authentication failed. Please check your credentials and try again.';
case 153:
return 'Operation cancelled by user.';
default:
return `Operation failed with error code: ${errorCode}`;
}
}
}
// Export singleton instance
const DataSigningService = new DataSigningServiceClass();
window.DataSigningService = DataSigningService;
Create a service to manage dropdown data and enum conversions:
// www/src/tutorial/screens/dataSigning/DropdownDataService.js
/**
* Service for managing dropdown data and enum conversions
*/
class DropdownDataServiceClass {
/**
* Get authentication level options for dropdown
* Only includes levels supported for data signing: 0, 1, and 4
* @returns {Array<{value: string, description: string}>}
*/
getAuthLevelOptions() {
return [
{ value: 'NONE (0)', description: 'No Authentication' },
{ value: 'RDNA_AUTH_LEVEL_1 (1)', description: 'Re-Authentication' },
{ value: 'RDNA_AUTH_LEVEL_4 (4)', description: 'Maximum Security (Recommended)' }
// Note: Levels 2 and 3 are NOT SUPPORTED for data signing
];
}
/**
* Get authenticator type options for dropdown
* Only includes types supported for data signing: 0 and 1
* @returns {Array<{value: string, description: string}>}
*/
getAuthenticatorTypeOptions() {
return [
{ value: 'NONE (0)', description: 'Auto-select best available' },
{ value: 'RDNA_IDV_SERVER_BIOMETRIC (1)', description: 'Server Biometric Verification' }
// Note: RDNA_AUTH_PASS (2) and RDNA_AUTH_LDA (3) are NOT SUPPORTED for data signing
];
}
/**
* Convert auth level display value to number
* @param {string} displayValue - e.g., "RDNA_AUTH_LEVEL_4 (4)"
* @returns {number} Numeric value (0, 1, or 4)
*/
convertAuthLevelToNumber(displayValue) {
if (displayValue.includes('(0)')) return 0;
if (displayValue.includes('(1)')) return 1;
if (displayValue.includes('(4)')) return 4;
// Default to Level 4 for maximum security
return 4;
}
/**
* Convert authenticator type display value to number
* @param {string} displayValue
* @returns {number} Numeric value (0 or 1)
*/
convertAuthenticatorTypeToNumber(displayValue) {
if (displayValue.includes('(0)')) return 0;
if (displayValue.includes('(1)')) return 1;
// Default to biometric for maximum security
return 1;
}
/**
* Validate auth level display value
* @param {string} displayValue
* @returns {boolean}
*/
isValidAuthLevel(displayValue) {
const validValues = this.getAuthLevelOptions().map(option => option.value);
return validValues.includes(displayValue);
}
/**
* Validate authenticator type display value
* @param {string} displayValue
* @returns {boolean}
*/
isValidAuthenticatorType(displayValue) {
const validValues = this.getAuthenticatorTypeOptions().map(option => option.value);
return validValues.includes(displayValue);
}
}
// Export singleton instance
const DropdownDataService = new DropdownDataServiceClass();
window.DropdownDataService = DropdownDataService;
Key error handling strategies in the service layer:
Now let's implement the user interface components for data signing using Cordova's SPA (Single Page Application) architecture with HTML templates and JavaScript modules.
Cordova applications use a different UI pattern than React Native:
template tags in index.htmlonContentLoaded() lifecycleThis is the primary screen where users input data to be signed.
The following image showcases the data signing input screen from the sample application:

<!-- www/index.html - Add this template -->
<template id="DataSigningInput-template">
<div class="screen-container">
<!-- Header with menu button -->
<div class="header">
<button id="data-signing-menu-button" class="menu-button">โฐ</button>
<h1 class="title">Data Signing</h1>
<p class="subtitle">Sign your data with cryptographic authentication</p>
</div>
<!-- Info Section -->
<div class="info-section">
<div class="info-title">How it works:</div>
<div class="info-text">
1. Enter your data payload and select authentication parameters<br>
2. Click "Sign Data" to initiate the signing process<br>
3. Complete authentication when prompted<br>
4. Receive your cryptographically signed data
</div>
</div>
<!-- Error Display -->
<div id="data-signing-error" class="error-banner" style="display: none;"></div>
<!-- Form -->
<div class="form-container">
<!-- Payload Input -->
<div class="input-group">
<label class="input-label">Data Payload *</label>
<textarea
id="data-signing-payload"
class="input-field multiline-input"
placeholder="Enter the data you want to sign..."
maxlength="500"
rows="4"
></textarea>
<div class="char-count">
<span id="data-signing-payload-count">0/500</span>
</div>
</div>
<!-- Auth Level Dropdown -->
<div class="input-group">
<label class="input-label">Authentication Level *</label>
<select id="data-signing-auth-level" class="input-field select-field">
<option value="">Select Authentication Level</option>
<!-- Options populated by JavaScript -->
</select>
<div class="help-text">Level 4 is recommended for maximum security</div>
</div>
<!-- Authenticator Type Dropdown -->
<div class="input-group">
<label class="input-label">Authenticator Type *</label>
<select id="data-signing-authenticator-type" class="input-field select-field">
<option value="">Select Authenticator Type</option>
<!-- Options populated by JavaScript -->
</select>
<div class="help-text">Choose the authentication method for signing</div>
</div>
<!-- Reason Input -->
<div class="input-group">
<label class="input-label">Signing Reason *</label>
<input
id="data-signing-reason"
type="text"
class="input-field"
placeholder="Enter reason for signing"
maxlength="100"
/>
<div class="char-count">
<span id="data-signing-reason-count">0/100</span>
</div>
</div>
</div>
<!-- Submit Button -->
<button id="data-signing-submit-btn" class="primary-button">
Sign Data
</button>
</div>
</template>
// www/src/tutorial/screens/dataSigning/DataSigningInputScreen.js
/**
* Data Signing Input Screen
* Screen for collecting data signing parameters from user
*
* SPA Lifecycle:
* - onContentLoaded(params) - Called when template loaded
* - setupEventListeners() - Attach form handlers
* - registerSDKEventHandlers() - Register data signing response handler
* - cleanup() - Clean up when navigating away
*/
const DataSigningInputScreen = {
// Form state
payload: '',
selectedAuthLevel: '',
selectedAuthenticatorType: '',
reason: '',
isLoading: false,
// Session info (from params)
userID: '',
sessionID: '',
// Original handlers (preservation pattern)
originalDataSigningHandler: null,
/**
* Called when screen content is loaded (SPA lifecycle)
* @param {Object} params - Navigation parameters
*/
onContentLoaded(params) {
console.log('DataSigningInputScreen - Content loaded');
// Store session info
this.userID = params.userID || '';
this.sessionID = params.sessionID || '';
// Reset form state
this.payload = '';
this.selectedAuthLevel = '';
this.selectedAuthenticatorType = '';
this.reason = '';
this.isLoading = false;
// Setup UI
this.setupEventListeners();
this.registerSDKEventHandlers();
this.populateDropdowns();
this.clearForm();
this.hideError();
this.updateCharCounts();
},
/**
* Setup event listeners for form elements
*/
setupEventListeners() {
// Menu button
const menuButton = document.getElementById('data-signing-menu-button');
if (menuButton) {
menuButton.onclick = () => NavigationService.openDrawer();
}
// Form submit button
const submitBtn = document.getElementById('data-signing-submit-btn');
if (submitBtn) {
submitBtn.onclick = this.handleSubmit.bind(this);
}
// Payload input with character count
const payloadInput = document.getElementById('data-signing-payload');
if (payloadInput) {
payloadInput.oninput = () => {
this.payload = payloadInput.value;
this.updateCharCounts();
};
}
// Auth level dropdown
const authLevelSelect = document.getElementById('data-signing-auth-level');
if (authLevelSelect) {
authLevelSelect.onchange = () => {
this.selectedAuthLevel = authLevelSelect.value;
};
}
// Authenticator type dropdown
const authenticatorTypeSelect = document.getElementById('data-signing-authenticator-type');
if (authenticatorTypeSelect) {
authenticatorTypeSelect.onchange = () => {
this.selectedAuthenticatorType = authenticatorTypeSelect.value;
};
}
// Reason input with character count
const reasonInput = document.getElementById('data-signing-reason');
if (reasonInput) {
reasonInput.oninput = () => {
this.reason = reasonInput.value;
this.updateCharCounts();
};
}
},
/**
* Register SDK event handlers for data signing
*/
registerSDKEventHandlers() {
const eventManager = rdnaService.getEventManager();
// Preserve original handler
this.originalDataSigningHandler = eventManager.dataSigningResponseHandler;
// Register data signing response handler
eventManager.setDataSigningResponseHandler((data) => {
this.handleDataSigningResponse(data);
});
},
/**
* Populate dropdowns with options
*/
populateDropdowns() {
// Populate auth level dropdown
const authLevelSelect = document.getElementById('data-signing-auth-level');
if (authLevelSelect && DropdownDataService) {
const options = DropdownDataService.getAuthLevelOptions();
authLevelSelect.innerHTML = '<option value="">Select Authentication Level</option>';
options.forEach(option => {
const optionElement = document.createElement('option');
optionElement.value = option.value;
optionElement.textContent = \`\${option.value} - \${option.description}\`;
authLevelSelect.appendChild(optionElement);
});
}
// Populate authenticator type dropdown
const authenticatorTypeSelect = document.getElementById('data-signing-authenticator-type');
if (authenticatorTypeSelect && DropdownDataService) {
const options = DropdownDataService.getAuthenticatorTypeOptions();
authenticatorTypeSelect.innerHTML = '<option value="">Select Authenticator Type</option>';
options.forEach(option => {
const optionElement = document.createElement('option');
optionElement.value = option.value;
optionElement.textContent = \`\${option.value} - \${option.description}\`;
authenticatorTypeSelect.appendChild(optionElement);
});
}
},
/**
* Handle form submission
*/
async handleSubmit() {
console.log('DataSigningInputScreen - Submit button clicked');
// Validate form
const validation = DataSigningService.validateSigningInput(
this.payload,
this.selectedAuthLevel,
this.selectedAuthenticatorType,
this.reason
);
if (!validation.isValid) {
this.showError(validation.errors.join(', '));
return;
}
// Hide error and show loading
this.hideError();
this.setLoading(true);
try {
// Set context for password authentication
DataSigningSetupAuthManager.setContext({
payload: this.payload,
authLevel: DropdownDataService.convertAuthLevelToNumber(this.selectedAuthLevel),
authenticatorType: DropdownDataService.convertAuthenticatorTypeToNumber(this.selectedAuthenticatorType),
reason: this.reason
});
// Call DataSigningService
await DataSigningService.signData(
this.payload,
this.selectedAuthLevel,
this.selectedAuthenticatorType,
this.reason
);
console.log('DataSigningInputScreen - SignData API call successful');
// Result will come via onAuthenticateUserAndSignData event
} catch (error) {
console.error('DataSigningInputScreen - Submit error:', error);
this.setLoading(false);
this.showError(error?.error?.errorString || 'Failed to submit request');
DataSigningSetupAuthManager.clearContext();
}
},
/**
* Handle data signing response event
*/
handleDataSigningResponse(data) {
console.log('DataSigningInputScreen - Processing data signing response');
this.setLoading(false);
// Hide password modal
if (window.PasswordChallengeModal) {
PasswordChallengeModal.hide();
}
// Check for errors
if (data.error && data.error.longErrorCode !== 0) {
this.showError(DataSigningService.getErrorMessage(data.error.longErrorCode));
DataSigningSetupAuthManager.handleError(data.error);
return;
}
// Check status code
if (data.status && data.status.statusCode !== 100) {
this.showError(\`Signing failed: \${data.status.statusMessage}\`);
DataSigningSetupAuthManager.handleError(data);
return;
}
// Success! Navigate to result screen
console.log('DataSigningInputScreen - Data signing successful');
DataSigningSetupAuthManager.handleSuccess(data);
NavigationService.navigate('DataSigningResult', {
resultData: data
});
},
/**
* Update character count displays
*/
updateCharCounts() {
const payloadCount = document.getElementById('data-signing-payload-count');
const reasonCount = document.getElementById('data-signing-reason-count');
if (payloadCount) {
payloadCount.textContent = \`\${this.payload.length}/500\`;
}
if (reasonCount) {
reasonCount.textContent = \`\${this.reason.length}/100\`;
}
},
/**
* Show error message
*/
showError(message) {
const errorDiv = document.getElementById('data-signing-error');
if (errorDiv) {
errorDiv.textContent = message;
errorDiv.style.display = 'block';
}
},
/**
* Hide error message
*/
hideError() {
const errorDiv = document.getElementById('data-signing-error');
if (errorDiv) {
errorDiv.style.display = 'none';
}
},
/**
* Set loading state
*/
setLoading(loading) {
this.isLoading = loading;
const submitBtn = document.getElementById('data-signing-submit-btn');
if (submitBtn) {
submitBtn.disabled = loading;
submitBtn.textContent = loading ? 'Processing...' : 'Sign Data';
}
},
/**
* Clear form fields
*/
clearForm() {
const payloadInput = document.getElementById('data-signing-payload');
const authLevelSelect = document.getElementById('data-signing-auth-level');
const authenticatorTypeSelect = document.getElementById('data-signing-authenticator-type');
const reasonInput = document.getElementById('data-signing-reason');
if (payloadInput) payloadInput.value = '';
if (authLevelSelect) authLevelSelect.value = '';
if (authenticatorTypeSelect) authenticatorTypeSelect.value = '';
if (reasonInput) reasonInput.value = '';
this.updateCharCounts();
},
/**
* Cleanup when navigating away
*/
cleanup() {
const eventManager = rdnaService.getEventManager();
if (this.originalDataSigningHandler) {
eventManager.setDataSigningResponseHandler(this.originalDataSigningHandler);
}
}
};
// Make available globally
window.DataSigningInputScreen = DataSigningInputScreen;
The following image showcases the successful data signing results screen:

<!-- www/index.html - Add this template -->
<template id="DataSigningResult-template">
<div class="screen-container">
<!-- Success Header -->
<div class="success-header">
<div class="success-icon">โ
</div>
<h1 class="success-title">Data Signing Successful!</h1>
<p class="success-subtitle">Your data has been cryptographically signed</p>
</div>
<!-- Results Section -->
<div class="results-section">
<h2 class="section-title">Signing Results</h2>
<p class="section-subtitle">All values below have been cryptographically verified</p>
<!-- Results Container (populated by JavaScript) -->
<div id="data-signing-results-container"></div>
</div>
<!-- Actions Section -->
<div class="actions-section">
<button id="data-signing-sign-another-btn" class="primary-button">
๐ Sign Another Document
</button>
</div>
</div>
</template>
// www/src/tutorial/screens/dataSigning/DataSigningResultScreen.js
const DataSigningResultScreen = {
resultData: null,
resultDisplay: null,
copiedField: null,
/**
* Called when screen content is loaded
*/
onContentLoaded(params) {
console.log('DataSigningResultScreen - Content loaded');
this.resultData = params?.resultData;
if (!this.resultData) {
this.showError('No signing results available');
return;
}
// Format for display
this.resultDisplay = DataSigningService.formatSigningResultForDisplay(this.resultData);
// Setup UI
this.setupEventListeners();
this.renderResults();
},
/**
* Setup event listeners
*/
setupEventListeners() {
const signAnotherBtn = document.getElementById('data-signing-sign-another-btn');
if (signAnotherBtn) {
signAnotherBtn.onclick = this.handleSignAnother.bind(this);
}
},
/**
* Render results dynamically
*/
renderResults() {
const resultsContainer = document.getElementById('data-signing-results-container');
if (!resultsContainer) return;
const resultItems = DataSigningService.convertToResultInfoItems(this.resultDisplay);
let html = '';
resultItems.forEach((item) => {
const isSignature = item.name === 'Payload Signature';
html += \`
<div class="result-card \${isSignature ? 'signature-card' : ''}">
<div class="result-card-header">
<span class="result-card-label">\${item.name}</span>
\${item.value !== 'N/A' ? \`
<button class="copy-btn" data-field="\${item.name}" data-value="\${this.escapeHtml(item.value)}">
๐ Copy
</button>
\` : ''}
</div>
<div class="result-card-value">
\${this.escapeHtml(item.value)}
</div>
</div>
\`;
});
resultsContainer.innerHTML = html;
// Attach copy handlers
const copyBtns = resultsContainer.querySelectorAll('.copy-btn');
copyBtns.forEach(btn => {
btn.onclick = () => {
const fieldName = btn.getAttribute('data-field');
const value = btn.getAttribute('data-value');
this.handleCopyToClipboard(value, fieldName, btn);
};
});
},
/**
* Handle copy to clipboard
*/
async handleCopyToClipboard(value, fieldName, button) {
try {
// Modern Clipboard API
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(value);
} else {
// Fallback
const textArea = document.createElement('textarea');
textArea.value = value;
textArea.style.position = 'fixed';
textArea.style.left = '-9999px';
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
}
// Show success
button.textContent = 'โ Copied';
button.classList.add('copied');
setTimeout(() => {
button.textContent = '๐ Copy';
button.classList.remove('copied');
}, 2000);
} catch (error) {
console.error('Copy failed:', error);
alert('Failed to copy. Please copy manually.');
}
},
/**
* Handle "Sign Another Document"
*/
async handleSignAnother() {
try {
await DataSigningService.resetState();
NavigationService.navigate('DataSigningInput', {
userID: this.userID,
sessionID: this.sessionID
});
} catch (error) {
NavigationService.navigate('DataSigningInput');
}
},
/**
* Show error
*/
showError(message) {
const container = document.getElementById('data-signing-results-container');
if (container) {
container.innerHTML = \`
<div class="error-container">
<div class="error-icon">โ ๏ธ</div>
<div class="error-message">\${this.escapeHtml(message)}</div>
<button class="back-btn" onclick="NavigationService.navigate('DataSigningInput')">
Back to Data Signing
</button>
</div>
\`;
}
},
/**
* Escape HTML
*/
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
};
// Make available globally
window.DataSigningResultScreen = DataSigningResultScreen;
When data signing requires password verification (challengeMode 12), a modal dialog prompts the user for authentication.
The following image showcases the password challenge modal during data signing:

<!-- www/index.html - Add this modal div (not a template, persistent in DOM) -->
<div id="data-signing-password-modal" class="modal-overlay" style="display: none;">
<div class="modal-container">
<!-- Modal content populated by JavaScript -->
<div id="data-signing-password-modal-content"></div>
</div>
</div>
/* www/css/style.css - Add modal styles */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.modal-container {
background: white;
border-radius: 12px;
width: 90%;
max-width: 400px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.password-modal-header {
padding: 20px;
border-bottom: 1px solid #eee;
}
.password-modal-title {
font-size: 18px;
font-weight: 600;
color: #1a1a1a;
margin: 0;
}
.password-modal-body {
padding: 20px;
}
.password-modal-context {
background-color: #f8f9fa;
border-radius: 8px;
padding: 12px;
margin-bottom: 16px;
}
.password-modal-context-label {
font-size: 12px;
color: #666;
margin-bottom: 4px;
}
.password-modal-context-value {
font-size: 14px;
color: #1a1a1a;
font-weight: 500;
}
.password-modal-attempts {
background-color: #e8f4fd;
border-radius: 8px;
padding: 12px;
margin-bottom: 16px;
text-align: center;
}
.password-modal-attempts.warning {
background-color: #fff3cd;
}
.password-modal-attempts.danger {
background-color: #f8d7da;
}
.password-modal-attempts-text {
font-size: 14px;
font-weight: 500;
color: #1a1a1a;
}
.password-modal-error {
background-color: #f8d7da;
border-radius: 8px;
padding: 12px;
margin-bottom: 16px;
}
.password-modal-error-text {
font-size: 14px;
color: #721c24;
}
.password-modal-input-group {
margin-bottom: 16px;
}
.password-modal-label {
display: block;
font-size: 14px;
font-weight: 500;
color: #1a1a1a;
margin-bottom: 8px;
}
.password-modal-input-wrapper {
position: relative;
}
.password-modal-input {
width: 100%;
padding: 12px;
padding-right: 40px;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 16px;
box-sizing: border-box;
}
.password-modal-input:focus {
outline: none;
border-color: #007AFF;
}
.password-modal-toggle-btn {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
padding: 8px;
cursor: pointer;
font-size: 18px;
}
.password-modal-footer {
padding: 20px;
border-top: 1px solid #eee;
display: flex;
gap: 12px;
}
.password-modal-btn {
flex: 1;
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: opacity 0.2s;
}
.password-modal-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.password-modal-btn-primary {
background-color: #007AFF;
color: white;
}
.password-modal-btn-secondary {
background-color: #f0f0f0;
color: #1a1a1a;
}
// www/src/tutorial/screens/dataSigning/PasswordChallengeModal.js
/**
* Password Challenge Modal for Data Signing
*
* Shows modal when SDK triggers getPassword event with challengeMode 12
* during data signing authentication flow.
*
* Features:
* - Password input with visibility toggle
* - Attempts counter with color coding (green โ orange โ red)
* - Error display from SDK responses
* - Submit and Cancel actions
* - Auto-focus on password input
* - Keyboard handling (Enter to submit)
*/
const PasswordChallengeModal = {
// State
visible: false,
challengeMode: 12,
attemptsLeft: 3,
errorMessage: '',
isSubmitting: false,
passwordVisible: false,
// Context (what's being signed)
context: {
payload: '',
reason: ''
},
/**
* Show modal with configuration
* @param {number} challengeMode - Challenge mode (should be 12 for data signing)
* @param {number} attemptsLeft - Remaining authentication attempts
* @param {Object} context - Signing context (payload, reason)
*/
show(challengeMode, attemptsLeft, context) {
console.log('PasswordChallengeModal - Showing modal:', {
challengeMode,
attemptsLeft,
hasContext: !!context
});
this.visible = true;
this.challengeMode = challengeMode;
this.attemptsLeft = attemptsLeft;
this.context = context || {};
this.errorMessage = '';
this.isSubmitting = false;
this.passwordVisible = false;
// Render and show
this.render();
const modalElement = document.getElementById('data-signing-password-modal');
if (modalElement) {
modalElement.style.display = 'flex';
}
// Auto-focus password input after modal renders
setTimeout(() => {
const passwordInput = document.getElementById('password-modal-input');
if (passwordInput) {
passwordInput.focus();
}
}, 300);
},
/**
* Update modal state (for re-triggered getPassword events)
* @param {Object} updates - State updates
*/
update(updates) {
console.log('PasswordChallengeModal - Updating modal:', updates);
if (updates.attemptsLeft !== undefined) {
this.attemptsLeft = updates.attemptsLeft;
}
if (updates.errorMessage !== undefined) {
this.errorMessage = updates.errorMessage;
}
// Re-render with updated state
this.render();
// Clear and refocus password input
const passwordInput = document.getElementById('password-modal-input');
if (passwordInput) {
passwordInput.value = '';
passwordInput.focus();
}
},
/**
* Hide modal and cleanup
*/
hide() {
console.log('PasswordChallengeModal - Hiding modal');
this.visible = false;
this.errorMessage = '';
this.isSubmitting = false;
this.passwordVisible = false;
const modalElement = document.getElementById('data-signing-password-modal');
if (modalElement) {
modalElement.style.display = 'none';
}
// Clear password input
const passwordInput = document.getElementById('password-modal-input');
if (passwordInput) {
passwordInput.value = '';
}
},
/**
* Render modal HTML dynamically
*/
render() {
const contentElement = document.getElementById('data-signing-password-modal-content');
if (!contentElement) return;
// Determine attempts color
const attemptsClass = this.getAttemptsClass();
const attemptsColor = this.getAttemptsColor();
contentElement.innerHTML = \`
<div class="password-modal-header">
<h2 class="password-modal-title">๐ Authentication Required</h2>
</div>
<div class="password-modal-body">
<!-- Context: What's being signed -->
\${this.context.reason ? \`
<div class="password-modal-context">
<div class="password-modal-context-label">Signing Reason:</div>
<div class="password-modal-context-value">\${this.escapeHtml(this.context.reason)}</div>
</div>
\` : ''}
<!-- Attempts Counter -->
\${this.attemptsLeft <= 3 ? \`
<div class="password-modal-attempts \${attemptsClass}">
<div class="password-modal-attempts-text" style="color: \${attemptsColor};">
\${this.attemptsLeft} attempt\${this.attemptsLeft !== 1 ? 's' : ''} remaining
</div>
</div>
\` : ''}
<!-- Error Display -->
\${this.errorMessage ? \`
<div class="password-modal-error">
<div class="password-modal-error-text">\${this.escapeHtml(this.errorMessage)}</div>
</div>
\` : ''}
<!-- Password Input -->
<div class="password-modal-input-group">
<label class="password-modal-label">Password</label>
<div class="password-modal-input-wrapper">
<input
type="\${this.passwordVisible ? 'text' : 'password'}"
id="password-modal-input"
class="password-modal-input"
placeholder="Enter your password"
\${this.isSubmitting ? 'disabled' : ''}
/>
<button
id="password-modal-toggle-btn"
class="password-modal-toggle-btn"
type="button"
\${this.isSubmitting ? 'disabled' : ''}
>
\${this.passwordVisible ? '๐๏ธ' : '๐'}
</button>
</div>
</div>
</div>
<div class="password-modal-footer">
<button
id="password-modal-cancel-btn"
class="password-modal-btn password-modal-btn-secondary"
\${this.isSubmitting ? 'disabled' : ''}
>
Cancel
</button>
<button
id="password-modal-submit-btn"
class="password-modal-btn password-modal-btn-primary"
\${this.isSubmitting ? 'disabled' : ''}
>
\${this.isSubmitting ? 'Verifying...' : 'Verify & Continue'}
</button>
</div>
\`;
// Attach event listeners
this.attachEventListeners();
},
/**
* Attach event listeners to modal elements
*/
attachEventListeners() {
// Submit button
const submitBtn = document.getElementById('password-modal-submit-btn');
if (submitBtn) {
submitBtn.onclick = () => this.handleSubmit();
}
// Cancel button
const cancelBtn = document.getElementById('password-modal-cancel-btn');
if (cancelBtn) {
cancelBtn.onclick = () => this.handleCancel();
}
// Toggle visibility button
const toggleBtn = document.getElementById('password-modal-toggle-btn');
if (toggleBtn) {
toggleBtn.onclick = () => this.togglePasswordVisibility();
}
// Password input - Enter key to submit
const passwordInput = document.getElementById('password-modal-input');
if (passwordInput) {
passwordInput.onkeypress = (e) => {
if (e.key === 'Enter' && !this.isSubmitting) {
this.handleSubmit();
}
};
}
},
/**
* Handle submit button click
*/
handleSubmit() {
if (this.isSubmitting) return;
const passwordInput = document.getElementById('password-modal-input');
const password = passwordInput?.value.trim();
if (!password) {
this.errorMessage = 'Please enter your password';
this.render();
return;
}
console.log('PasswordChallengeModal - Submitting password');
this.isSubmitting = true;
this.render();
// Call DataSigningService to submit password
DataSigningService.submitPassword(password, this.challengeMode)
.then(() => {
console.log('PasswordChallengeModal - Password submitted successfully');
// Keep modal visible - SDK will trigger response event
})
.catch((error) => {
console.error('PasswordChallengeModal - Password submission failed:', error);
this.isSubmitting = false;
this.errorMessage = error?.error?.errorString || 'Authentication failed';
this.render();
});
},
/**
* Handle cancel button click
*/
handleCancel() {
console.log('PasswordChallengeModal - Cancel clicked');
// Call DataSigningService to reset state
DataSigningService.resetState()
.then(() => {
this.hide();
})
.catch((error) => {
console.error('PasswordChallengeModal - Reset failed:', error);
this.hide(); // Hide anyway
});
},
/**
* Toggle password visibility
*/
togglePasswordVisibility() {
this.passwordVisible = !this.passwordVisible;
this.render();
// Maintain focus
const passwordInput = document.getElementById('password-modal-input');
if (passwordInput) {
passwordInput.focus();
}
},
/**
* Get attempts CSS class based on count
* @returns {string} CSS class
*/
getAttemptsClass() {
if (this.attemptsLeft === 1) return 'danger';
if (this.attemptsLeft === 2) return 'warning';
return '';
},
/**
* Get attempts color based on count
* @returns {string} Color code
*/
getAttemptsColor() {
if (this.attemptsLeft === 1) return '#dc2626'; // Red
if (this.attemptsLeft === 2) return '#f59e0b'; // Orange
return '#10b981'; // Green
},
/**
* Escape HTML to prevent XSS
* @param {string} text - Text to escape
* @returns {string} Escaped text
*/
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
};
// Make available globally
if (typeof window !== 'undefined') {
window.PasswordChallengeModal = PasswordChallengeModal;
}
The modal is controlled by the DataSigningSetupAuthManager, which handles challengeMode 12 events:
// www/src/uniken/managers/DataSigningSetupAuthManager.js
/**
* Manager for handling data signing authentication context and password challenges
*
* Responsibilities:
* - Store data signing context (payload, auth level, etc.)
* - Show/hide password challenge modal
* - Handle password submission with challengeMode 12
* - Handle cancellation and cleanup
*/
const DataSigningSetupAuthManager = {
// Context storage
context: null,
/**
* Set signing context (called before initiating signing)
* @param {Object} context - Signing parameters
*/
setContext(context) {
console.log('DataSigningSetupAuthManager - Setting context');
this.context = {
payload: context.payload,
authLevel: context.authLevel,
authenticatorType: context.authenticatorType,
reason: context.reason,
userID: context.userID || ''
};
},
/**
* Get stored context
* @returns {Object|null}
*/
getContext() {
return this.context;
},
/**
* Clear stored context
*/
clearContext() {
console.log('DataSigningSetupAuthManager - Clearing context');
this.context = null;
},
/**
* Check if manager has active context
* @returns {boolean}
*/
isActive() {
return this.context !== null;
},
/**
* Show password challenge modal
* Called when SDK triggers getPassword with challengeMode 12
* @param {Object} data - getPassword event data
*/
showPasswordDialog(data) {
console.log('DataSigningSetupAuthManager - Showing password dialog');
// Check if this is first time or re-trigger
if (PasswordChallengeModal.visible) {
// Re-trigger: Update with new error/attempts
PasswordChallengeModal.update({
attemptsLeft: data.attemptsLeft,
errorMessage: data.challengeResponse?.status?.statusMessage || ''
});
} else {
// First time: Show modal
PasswordChallengeModal.show(
data.challengeMode,
data.attemptsLeft,
{
payload: this.context?.payload,
reason: this.context?.reason
}
);
}
},
/**
* Hide password dialog
*/
hidePasswordDialog() {
PasswordChallengeModal.hide();
},
/**
* Handle successful signing (called from DataSigningInputScreen)
* @param {Object} signingResult - Signing response data
*/
handleSuccess(signingResult) {
console.log('DataSigningSetupAuthManager - Handling success');
this.clearContext();
this.hidePasswordDialog();
},
/**
* Handle signing error (called from DataSigningInputScreen)
* @param {Object} error - Error data
*/
handleError(error) {
console.log('DataSigningSetupAuthManager - Handling error');
this.clearContext();
this.hidePasswordDialog();
}
};
// Make available globally
if (typeof window !== 'undefined') {
window.DataSigningSetupAuthManager = DataSigningSetupAuthManager;
}
The SDKEventProvider routes challengeMode 12 events to the manager:
// www/src/uniken/providers/SDKEventProvider.js (Data Signing Section)
/**
* Handle getPassword event - route based on challengeMode
*/
handleGetPassword(data) {
console.log('SDKEventProvider - getPassword event, challengeMode:', data.challengeMode);
// ... other challengeMode handling (0, 1, 2, 3, 4, etc.) ...
// challengeMode 12: Data signing password verification
else if (data.challengeMode === 12) {
if (typeof DataSigningSetupAuthManager !== 'undefined' && DataSigningSetupAuthManager.isActive()) {
DataSigningSetupAuthManager.showPasswordDialog(data);
} else {
console.warn('SDKEventProvider - DataSigningSetupAuthManager not active for challengeMode 12');
}
}
}
1. User submits signing form
โ
2. SDK determines authentication needed
โ
3. SDK triggers getPassword with challengeMode 12
โ
4. SDKEventProvider routes to DataSigningSetupAuthManager
โ
5. Manager shows PasswordChallengeModal
โ
6. User enters password and submits
โ
7. DataSigningService.submitPassword() called
โ
8a. SUCCESS: SDK triggers onAuthenticateUserAndSignData
โ Modal stays visible until response
โ DataSigningInputScreen navigates to results
โ Modal hidden
8b. WRONG PASSWORD: SDK re-triggers getPassword
โ Modal updates with error and decremented attempts
โ User can retry
8c. CANCEL: User clicks Cancel button
โ DataSigningService.resetState() called
โ Modal hidden
โ Form returns to editable state
Let's explore comprehensive testing strategies for your data signing implementation.
Test these key scenarios to ensure complete functionality:
Manual Testing Steps:
Console Output Pattern:
DataSigningInputScreen - Submit button clicked
DataSigningService - Starting data signing process
RdnaService - Initiating data signing...
RdnaService - AuthenticateUserAndSignData sync response success
[Password challenge if required]
RdnaEventManager - onAuthenticateUserAndSignData event received
DataSigningInputScreen - Data signing successful
Error Scenarios to Test:
// Error Code 102: Authentication failed
// Expected: Error message displayed, retry available
// Error Code 214: Authentication method not supported
// Expected: User-friendly message, suggest alternative method
// Error Code 153: Operation cancelled by user
// Expected: Clean state reset, return to input form
Testing Steps:
Testing Steps:
Scenarios:
resetAuthenticateUserAndSignDataState() calledTest all validation rules:
// Required field validation
Payload: "" โ "Payload is required"
Auth Level: "" โ "Please select an authentication level"
Authenticator Type: "" โ "Please select an authenticator type"
Reason: "" โ "Reason is required"
// Character limit enforcement
Payload: 501 chars โ "Payload must be less than 500 characters"
Reason: 101 chars โ "Reason must be less than 100 characters"
// Dropdown validation
Invalid auth level โ "Invalid authentication level"
Invalid authenticator โ "Invalid authenticator type"
Test unsupported levels:
// Levels 2 and 3 are NOT SUPPORTED
// Verify these options don't appear in dropdown
// If manually set, SDK should return error
// Test supported combinations:
Level 0 + Type 0 โ Should work (testing only)
Level 1 + Type 0 โ Should work (standard signing)
Level 4 + Type 1 โ Should work (max security)
// Test invalid combinations:
Level 4 + Type 0 โ Should fail with error 214
Level 4 + Type 2 โ Should fail (not in dropdown)
Verify all loading states:
Use this checklist to verify complete implementation:
Core Functionality:
Error Handling:
Security Validation:
UI/UX Quality:
Use these browser console commands for debugging:
// Check current screen
NavigationService.getCurrentRoute()
// Inspect form state
DataSigningInputScreen.payload
DataSigningInputScreen.selectedAuthLevel
DataSigningInputScreen.isLoading
// Test dropdown service
DropdownDataService.getAuthLevelOptions()
DropdownDataService.convertAuthLevelToNumber("RDNA_AUTH_LEVEL_4 (4)")
// Test validation
DataSigningService.validateSigningInput("test", "RDNA_AUTH_LEVEL_4 (4)", "RDNA_IDV_SERVER_BIOMETRIC (1)", "test reason")
// Manually trigger events
rdnaService.getEventManager().dataSigningResponseHandler({...testData})
"Can't find variable: com"
# Verify plugin installed
cordova plugin ls
# For LOCAL plugin
cordova plugin add ./RdnaClient
cordova prepare
# Rebuild
cordova build
"Plugin not found"
# Check installed plugins
cordova plugin ls
# For LOCAL plugin: Verify directory exists
ls -la ./RdnaClient
# Reinstall
cordova plugin remove cordova-plugin-rdna
cordova plugin add ./RdnaClient
cordova prepare
Changes Not Reflecting
# Always run prepare after www/ changes
cordova prepare
# Or rebuild completely
cordova build
Browser DevTools:
Useful Console Commands:
// Check plugin availability
typeof com !== 'undefined'
// Check specific method
typeof com.uniken.rdnaplugin.RdnaClient.authenticateUserAndSignData
// Inspect event manager state
rdnaService.getEventManager()
// Check screen state
DataSigningInputScreen.isLoading
DataSigningInputScreen.payload
// Test service methods
DataSigningService.getErrorMessage(214)
Logging Best Practices:
// Use structured logging for debugging
console.log('DataSigningInputScreen - Submit:', JSON.stringify({
payloadLength: payload.length,
authLevel,
authenticatorType,
reasonLength: reason.length
}, null, 2));
// Log all SDK responses
console.log('SDK Response:', JSON.stringify(result, null, 2));
Choose the appropriate authentication level based on data sensitivity:
Data Sensitivity | Level | Use Cases | Security Features |
Testing/Dev | 0 (NONE) | Development, testing | โ ๏ธ No authentication |
Standard | 1 | Public documents, low-risk | Device biometric/passcode/password |
High Security | 4 | Financial, legal, medical, compliance | Server biometric verification |
Implementation Guidelines:
// โ
GOOD: Use switch-based selection with clear use cases
function selectAuthLevel(documentType) {
switch (documentType) {
case 'financial':
case 'legal':
case 'medical':
// Use maximum security for sensitive data
return {
authLevel: 4, // RDNA_AUTH_LEVEL_4
authenticatorType: 1 // RDNA_IDV_SERVER_BIOMETRIC
};
case 'internal':
case 'approval':
// Use standard security for routine operations
return {
authLevel: 1, // RDNA_AUTH_LEVEL_1
authenticatorType: 0 // NONE (auto-select)
};
case 'testing':
// Only for development environments
if (process.env.NODE_ENV !== 'production') {
return {
authLevel: 0, // NONE
authenticatorType: 0
};
}
throw new Error('Testing mode not allowed in production');
default:
// Default to maximum security
return {
authLevel: 4,
authenticatorType: 1
};
}
}
// โ BAD: Using unsupported levels
const config = {
authLevel: 2, // NOT SUPPORTED - will cause SDK error
authenticatorType: 0
};
// โ BAD: Using wrong authenticator type with Level 4
const config = {
authLevel: 4,
authenticatorType: 0 // Must be 1 (RDNA_IDV_SERVER_BIOMETRIC)
};
Proper State Cleanup:
// โ
GOOD: Complete cleanup sequence
async function cleanupDataSigning() {
try {
// 1. Reset SDK authentication state
await DataSigningService.resetState();
// 2. Clear sensitive form data
DataSigningInputScreen.payload = '';
DataSigningInputScreen.reason = '';
// 3. Reset authentication modal state
PasswordChallengeModal.hide();
// 4. Clear results (optional - depends on requirements)
DataSigningResultScreen.resultData = null;
console.log('Data signing state cleaned successfully');
} catch (error) {
console.error('Cleanup failed:', error);
// Continue - cleanup failure should not block user
}
}
// โ BAD: Incomplete cleanup leaves sensitive data in memory
function badCleanup() {
// Only clears UI, SDK state remains
document.getElementById('data-signing-payload').value = '';
}
Memory Management for Sensitive Data:
// โ
GOOD: Clear password immediately after use
async function submitPassword(password, challengeMode) {
try {
const response = await rdnaService.setPassword(password, challengeMode);
// Clear password from memory immediately
password = '';
// Clear from modal state
PasswordChallengeModal.password = '';
return response;
} catch (error) {
// Still clear password on error
password = '';
throw error;
}
}
// โ BAD: Password persists in memory
const modalState = {
password: 'user-password', // Stored indefinitely
isVisible: true
};
Secure Error Message Display:
// โ
GOOD: Map error codes to user-friendly messages
function getErrorMessage(errorCode) {
const errorMessages = {
0: 'Success',
102: 'Authentication failed. Please check your credentials and try again.',
153: 'Operation cancelled by user.',
214: 'Authentication method not supported. Please try a different authentication type.',
// ... more mappings
};
const message = errorMessages[errorCode];
// Don't expose internal error details to users
if (!message) {
console.error('Unmapped error code:', errorCode); // Log for debugging
return 'An error occurred. Please try again.'; // Generic to user
}
return message;
}
// โ BAD: Exposing internal error details
function badErrorHandling(error) {
alert(\`Error: \${JSON.stringify(error)}\`); // Exposes stack traces, internal IDs
}
Comprehensive Error Recovery:
// โ
GOOD: Multi-layer error handling with recovery
async function handleDataSigningError(error) {
console.error('Data signing error:', JSON.stringify({
errorCode: error.error?.longErrorCode,
statusCode: error.status?.statusCode,
message: error.error?.errorString
}, null, 2));
// 1. Attempt SDK state cleanup
try {
await DataSigningService.resetState();
} catch (resetError) {
console.error('State reset failed:', resetError);
// Continue - don't block user
}
// 2. Clear sensitive local state
DataSigningInputScreen.payload = '';
DataSigningInputScreen.isLoading = false;
// 3. Show user-friendly error
const userMessage = DataSigningService.getErrorMessage(
error.error?.longErrorCode || 0
);
DataSigningInputScreen.showError(userMessage);
// 4. Provide retry opportunity
// Form remains editable for user to retry
}
Audit Logging Requirements:
// โ
GOOD: Comprehensive audit trail
async function auditDataSigning(request, response) {
const auditEntry = {
timestamp: new Date().toISOString(),
userId: request.userId,
payloadHash: hashPayload(request.payload), // Hash, not actual payload
authLevel: request.authLevel,
authenticatorType: request.authenticatorType,
success: response.error.longErrorCode === 0,
errorCode: response.error.longErrorCode,
signatureId: response.dataSignatureID,
sessionId: getCurrentSessionId()
};
// Send to audit logging service
await sendToAuditLog(auditEntry);
}
function hashPayload(payload) {
// Use SHA-256 for audit trail
// Actual implementation depends on environment
return crypto.subtle.digest('SHA-256', new TextEncoder().encode(payload))
.then(hash => Array.from(new Uint8Array(hash))
.map(b => b.toString(16).padStart(2, '0'))
.join(''));
}
// โ BAD: Logging sensitive data
console.log('Signing payload:', payload); // Exposes sensitive data in logs
Sensitive Data Handling:
// โ
GOOD: Minimal sensitive data retention
const signingRequest = {
payloadHash: hashData(payload), // Store hash instead of original
authLevel: 4,
authenticatorType: 1,
reason: reason,
timestamp: Date.now()
};
// After signing complete, immediately clear
function clearSensitiveData() {
signingRequest.payloadHash = null;
signingRequest.reason = null;
}
// โ BAD: Storing original sensitive data indefinitely
const appState = {
lastPayload: payload, // Persists in memory
lastSignature: signature, // Not encrypted
userPassword: password // Never store passwords
};
Data Validation:
// โ
GOOD: Comprehensive input validation
function validatePayload(payload) {
// Length check
if (payload.length > 500) {
throw new Error('Payload exceeds maximum length');
}
// Content validation
if (containsMaliciousContent(payload)) {
throw new Error('Invalid payload content');
}
// Checksum validation (if applicable)
if (!validateChecksum(payload)) {
throw new Error('Payload integrity check failed');
}
return true;
}
// โ BAD: No validation before processing
function badValidation(payload) {
// Blindly accepts any input
return true;
}
Industry-Specific Requirements:
Industry | Requirements | Implementation Notes |
Financial | SOX, PCI DSS, Basel III | Audit logs, encryption at rest/transit, MFA |
Healthcare | HIPAA, HITECH | Patient data protection, access controls, audit trails |
Government | FISMA, FedRAMP | High-security authentication (Level 4), certified encryption |
Legal | eIDAS, ESIGN Act | Legal-grade signatures, non-repudiation, timestamp authority |
Regulatory Compliance Checklist:
Optimization Patterns:
// โ
GOOD: Debounce user input for character counting
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
const updateCharCount = debounce(() => {
const count = document.getElementById('payload-count');
count.textContent = \`\${payload.length}/500\`;
}, 100);
// โ
GOOD: Cache dropdown options
const DropdownCache = {
authLevels: null,
authenticatorTypes: null,
getAuthLevels() {
if (!this.authLevels) {
this.authLevels = DropdownDataService.getAuthLevelOptions();
}
return this.authLevels;
}
};
Congratulations! You've successfully implemented secure cryptographic data signing with REL-ID SDK in your Cordova application.
You now have a production-grade data signing system with:
Before deploying to production, verify:
Security Validation:
Performance & Scalability:
Testing:
Immediate Enhancements:
Integration Opportunities:
Advanced Security:
REL-ID Documentation:
Thank you for completing the REL-ID Data Signing Flow codelab! ๐
You're now equipped to build secure, production-grade data signing features in your Cordova applications using REL-ID SDK's powerful cryptographic capabilities.