ECR Interface Protocol
1. Encrypt the data of the protocol
1.1 Get a 6-digit pair Pin Code
When the Wonder terminal enters the pair page, a 6-digit pair Pin Code(pinCode1) or the QR code generated by it will be displayed. The client device can obtain this string by scanning a code or manually entering it
Example:
const pinCode1 = '260880';
1.2 Encrypt the data of the protocol with AES256
- JavaScript
- Node.js
- Java
- Go
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>浏览器端AES加解密演示</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f5f7fa;
color: #333;
line-height: 1.6;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.header {
background-color: #4a90e2;
color: white;
padding: 20px;
text-align: center;
}
.header h1 {
font-size: 24px;
font-weight: 500;
}
.content {
padding: 30px;
}
.section {
margin-bottom: 30px;
padding: 20px;
border-radius: 8px;
border: 1px solid #e1e8ed;
background-color: #fafbfc;
}
.section-title {
font-size: 18px;
font-weight: 500;
margin-bottom: 15px;
color: #4a90e2;
border-bottom: 1px solid #e1e8ed;
padding-bottom: 8px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 500;
color: #555;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
font-family: inherit;
}
.form-group textarea {
resize: vertical;
min-height: 100px;
}
.btn {
background-color: #4a90e2;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
margin-right: 10px;
margin-bottom: 10px;
transition: background-color 0.3s;
}
.btn:hover {
background-color: #357abd;
}
.btn-secondary {
background-color: #6c757d;
}
.btn-secondary:hover {
background-color: #5a6268;
}
.result {
margin-top: 20px;
padding: 15px;
border-radius: 4px;
background-color: #e9ecef;
font-family: 'Courier New', Courier, monospace;
font-size: 14px;
word-break: break-all;
}
.result.success {
background-color: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
}
.result.error {
background-color: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
}
.test-results {
margin-top: 20px;
}
.test-item {
margin-bottom: 20px;
padding: 15px;
border-radius: 4px;
border: 1px solid #e1e8ed;
background-color: #fff;
}
.test-title {
font-weight: 500;
margin-bottom: 10px;
color: #4a90e2;
}
.test-details {
font-family: 'Courier New', Courier, monospace;
font-size: 13px;
margin-bottom: 8px;
}
.test-status {
font-weight: 500;
padding: 4px 8px;
border-radius: 4px;
display: inline-block;
}
.test-status.passed {
background-color: #d4edda;
color: #155724;
}
.test-status.failed {
background-color: #f8d7da;
color: #721c24;
}
.copy-btn {
background-color: #28a745;
color: white;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
margin-left: 10px;
transition: background-color 0.3s;
}
.copy-btn:hover {
background-color: #218838;
}
@media (max-width: 768px) {
body {
padding: 10px;
}
.content {
padding: 20px;
}
.btn {
width: 100%;
margin-right: 0;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>浏览器端AES-256-CBC加解密演示</h1>
</div>
<div class="content">
<!-- 手动加密解密区域 -->
<div class="section">
<div class="section-title">手动加密解密</div>
<div class="form-group">
<label for="key">密钥 (Password/Key):</label>
<input type="text" id="key" placeholder="请输入密钥" value="260880">
</div>
<div class="form-group">
<label for="plaintext">明文数据:</label>
<textarea id="plaintext" placeholder="请输入要加密的明文数据">123</textarea>
</div>
<div class="form-group">
<label for="ciphertext">密文数据:</label>
<textarea id="ciphertext" placeholder="请输入要解密的密文数据"></textarea>
</div>
<button class="btn" onclick="encryptData()">加密</button>
<button class="btn btn-secondary" onclick="decryptData()">解密</button>
<div id="manual-result" class="result"></div>
</div>
<!-- 自动测试区域 -->
<div class="section">
<div class="section-title">自动测试</div>
<button class="btn" onclick="runTests()">运行所有测试</button>
<div id="test-results" class="test-results"></div>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js"></script>
<script>
/**
* AES加密解密工具类
* 使用CryptoJS实现AES-256-CBC加密解密
* 包含PKCS7填充和PBKDF2密钥派生功能
*/
const AESCrypto = {
// 常量定义
BLOCK_SIZE: 16, // AES块大小(字节)
KEY_SIZE: 256 / 32, // 密钥大小(WordArray单位)
ITERATIONS: 100, // PBKDF2迭代次数
SALT_SIZE: 16, // 盐大小(字节)
IV_SIZE: 16, // IV大小(字节)
/**
* PKCS7填充函数
* @param {CryptoJS.lib.WordArray} wordArray - 要填充的数据
* @param {number} [blockSize=16] - 块大小
* @returns {CryptoJS.lib.WordArray} 填充后的数据
*/
pkcs7Pad: function(wordArray, blockSize = this.BLOCK_SIZE) {
if (!wordArray || typeof wordArray.sigBytes !== 'number') {
throw new Error('Invalid input: wordArray must be a valid CryptoJS WordArray');
}
const padding = blockSize - (wordArray.sigBytes % blockSize);
const paddingWords = [];
const paddingWord = ((padding & 0xff) << 24) | ((padding & 0xff) << 16) | ((padding & 0xff) << 8) | (padding & 0xff);
for (let i = 0; i < padding / 4; i++) {
paddingWords.push(paddingWord);
}
return wordArray.clone().concat(CryptoJS.lib.WordArray.create(paddingWords, padding));
},
/**
* PKCS7去填充函数
* @param {CryptoJS.lib.WordArray} wordArray - 要去填充的数据
* @returns {CryptoJS.lib.WordArray} 去填充后的数据
*/
pkcs7Unpad: function(wordArray) {
if (!wordArray || typeof wordArray.sigBytes !== 'number') {
throw new Error('Invalid input: wordArray must be a valid CryptoJS WordArray');
}
const sigBytes = wordArray.sigBytes;
if (sigBytes === 0) {
throw new Error('Invalid input: wordArray is empty');
}
// 获取最后一个字节作为填充长度
const lastWord = wordArray.words[Math.floor((sigBytes - 1) / 4)];
const padding = lastWord & 0xff;
// 验证填充长度
if (padding < 1 || padding > this.BLOCK_SIZE) {
throw new Error('Invalid PKCS7 padding: padding length out of range');
}
// 验证所有填充字节
for (let i = sigBytes - padding; i < sigBytes; i++) {
const currentWord = wordArray.words[Math.floor(i / 4)];
const currentByte = (currentWord >> (24 - (i % 4) * 8)) & 0xff;
if (currentByte !== padding) {
throw new Error('Invalid PKCS7 padding: inconsistent padding bytes');
}
}
// 创建去填充后的数据
return CryptoJS.lib.WordArray.create(wordArray.words, sigBytes - padding);
},
/**
* 使用PBKDF2派生密钥
* @param {string} pinCode - 密码/密钥
* @param {CryptoJS.lib.WordArray} salt - 盐值
* @returns {CryptoJS.lib.WordArray} 派生的密钥
*/
deriveKeySecure: function(pinCode, salt) {
if (!pinCode || !salt) {
throw new Error('Invalid input: pinCode and salt are required');
}
return CryptoJS.PBKDF2(pinCode, salt, {
keySize: this.KEY_SIZE,
iterations: this.ITERATIONS,
hasher: CryptoJS.algo.SHA256
});
},
/**
* 加密数据
* @param {string} pinCode - 密码/密钥
* @param {string} data - 要加密的明文数据
* @returns {string} Base64编码的加密结果
*/
encrypt: function(pinCode, data) {
try {
if (!pinCode) {
throw new Error('Invalid input: pinCode and data are required');
}
// 生成随机盐和IV
const salt = CryptoJS.lib.WordArray.random(this.SALT_SIZE);
const iv = CryptoJS.lib.WordArray.random(this.IV_SIZE);
// 派生密钥
const key = this.deriveKeySecure(pinCode, salt);
// 转换数据并填充
const dataWA = CryptoJS.enc.Utf8.parse(data);
const paddedData = this.pkcs7Pad(dataWA);
// 执行加密
const encrypted = CryptoJS.AES.encrypt(paddedData, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.NoPadding
});
// 拼接盐、IV和密文并Base64编码
return CryptoJS.enc.Base64.stringify(
salt.clone().concat(iv).concat(encrypted.ciphertext)
);
} catch (error) {
console.error('Encryption failed:', error);
throw error;
}
},
/**
* 解密数据
* @param {string} pinCode - 密码/密钥
* @param {string} encryptedBase64 - Base64编码的加密数据
* @returns {string} 解密后的明文数据
*/
decrypt: function(pinCode, encryptedBase64) {
try {
if (!pinCode || !encryptedBase64) {
throw new Error('Invalid input: pinCode and encryptedBase64 are required');
}
// 解析Base64数据
const encryptedData = CryptoJS.enc.Base64.parse(encryptedBase64);
// 验证数据长度
if (encryptedData.sigBytes < this.SALT_SIZE + this.IV_SIZE) {
throw new Error('Invalid encrypted data: data too short');
}
// 提取盐、IV和密文
const salt = CryptoJS.lib.WordArray.create(
encryptedData.words.slice(0, this.SALT_SIZE / 4),
this.SALT_SIZE
);
const iv = CryptoJS.lib.WordArray.create(
encryptedData.words.slice(this.SALT_SIZE / 4, (this.SALT_SIZE + this.IV_SIZE) / 4),
this.IV_SIZE
);
const ciphertext = CryptoJS.lib.WordArray.create(
encryptedData.words.slice((this.SALT_SIZE + this.IV_SIZE) / 4),
encryptedData.sigBytes - this.SALT_SIZE - this.IV_SIZE
);
// 派生密钥
const key = this.deriveKeySecure(pinCode, salt);
// 执行解密
const decrypted = CryptoJS.AES.decrypt({ ciphertext: ciphertext }, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.NoPadding
});
// 去填充并转换为字符串
const unpaddedData = this.pkcs7Unpad(decrypted);
return CryptoJS.enc.Utf8.stringify(unpaddedData);
} catch (error) {
console.error('Decryption failed:', error);
throw error;
}
}
};
/**
* 加密数据并显示结果
*/
function encryptData() {
const key = document.getElementById('key').value;
const plaintext = document.getElementById('plaintext').value;
const resultDiv = document.getElementById('manual-result');
try {
const encrypted = AESCrypto.encrypt(key, plaintext);
document.getElementById('ciphertext').value = encrypted;
resultDiv.innerHTML = ``;
resultDiv.className = 'result success';
} catch (error) {
resultDiv.innerHTML = ``;
resultDiv.className = 'result error';
}
}
/**
* 解密数据并显示结果
*/
function decryptData() {
const key = document.getElementById('key').value;
const ciphertext = document.getElementById('ciphertext').value;
const resultDiv = document.getElementById('manual-result');
try {
const decrypted = AESCrypto.decrypt(key, ciphertext);
resultDiv.innerHTML = ``;
resultDiv.className = 'result success';
} catch (error) {
resultDiv.innerHTML = ``;
resultDiv.className = 'result error';
}
}
/**
* 添加测试结果到UI
* @param {string} title - 测试标题
* @param {string} details - 测试详细信息
* @param {boolean} passed - 是否通过
*/
function addTestResult(title, details, passed) {
const testResultsDiv = document.getElementById('test-results');
const testItem = document.createElement('div');
testItem.className = 'test-item';
const testTitle = document.createElement('div');
testTitle.className = 'test-title';
testTitle.innerHTML = title;
const testDetails = document.createElement('div');
testDetails.className = 'test-details';
testDetails.innerHTML = details.replace(/\n/g, '<br>');
const testStatus = document.createElement('div');
testStatus.className = `test-status ${passed ? 'passed' : 'failed'}`;
testStatus.textContent = passed ? '通过' : '失败';
testItem.appendChild(testTitle);
testItem.appendChild(testDetails);
testItem.appendChild(testStatus);
testResultsDiv.appendChild(testItem);
}
/**
* 运行所有测试
*/
function runTests() {
// 清空之前的测试结果
document.getElementById('test-results').innerHTML = '';
console.log('=== AESCrypto 测试开始 ===');
let testCount = 0;
let passedCount = 0;
// 测试1: 基本功能测试
testCount++;
try {
const pinCode1 = '260880';
const data1 = '123';
const encrypted1 = AESCrypto.encrypt(pinCode1, data1);
const decrypted1 = AESCrypto.decrypt(pinCode1, encrypted1);
const passed = decrypted1 === data1;
if (passed) passedCount++;
const details = `密钥: ${pinCode1}\n明文: ${data1}\n密文: ${encrypted1}\n解密后: ${decrypted1}`;
addTestResult(`测试${testCount} - 基本功能测试`, details, passed);
} catch (error) {
addTestResult(`测试${testCount} - 基本功能测试`, error.message, false);
}
// 测试2: 复杂密钥和中文数据测试
testCount++;
try {
const pinCode2 = 'OvSdpyD2FoUNR5rNyte41QqZzR1Y4DVN';
const data2 = '这是一段中文测试数据';
const encrypted2 = AESCrypto.encrypt(pinCode2, data2);
const decrypted2 = AESCrypto.decrypt(pinCode2, encrypted2);
const passed = decrypted2 === data2;
if (passed) passedCount++;
const details = `密钥: ${pinCode2}\n明文: ${data2}\n密文: ${encrypted2}\n解密后: ${decrypted2}`;
addTestResult(`测试${testCount} - 复杂密钥和中文数据测试`, details, passed);
} catch (error) {
addTestResult(`测试${testCount} - 复杂密钥和中文数据测试`, error.message, false);
}
// 测试3: 空数据测试
testCount++;
try {
const pinCode3 = '260880';
const data3 = '';
const encrypted3 = AESCrypto.encrypt(pinCode3, data3);
const decrypted3 = AESCrypto.decrypt(pinCode3, encrypted3);
const passed = decrypted3 === data3;
if (passed) passedCount++;
const details = `密钥: ${pinCode3}\n明文: (空字符串)\n密文: ${encrypted3}\n解密后: (空字符串)`;
addTestResult(`测试${testCount} - 空数据测试`, details, passed);
} catch (error) {
addTestResult(`测试${testCount} - 空数据测试`, error.message, false);
}
// 测试4: 特殊字符测试
testCount++;
try {
const pinCode4 = '260880';
const data4 = '!@#$%^&*()_+-=[]{}|;:,.<>?';
const encrypted4 = AESCrypto.encrypt(pinCode4, data4);
const decrypted4 = AESCrypto.decrypt(pinCode4, encrypted4);
const passed = decrypted4 === data4;
if (passed) passedCount++;
const details = `密钥: ${pinCode4}\n明文: ${data4}\n密文: ${encrypted4}\n解密后: ${decrypted4}`;
addTestResult(`测试${testCount} - 特殊字符测试`, details, passed);
} catch (error) {
addTestResult(`测试${testCount} - 特殊字符测试`, error.message, false);
}
// 测试5: 长文本测试
testCount++;
try {
const pinCode5 = '260880';
const data5 = '这是一段较长的测试文本,用于测试AES加密解密的性能和正确性。这段文本包含了多种字符,包括中文、英文、数字和特殊符号。';
const encrypted5 = AESCrypto.encrypt(pinCode5, data5);
const decrypted5 = AESCrypto.decrypt(pinCode5, encrypted5);
const passed = decrypted5 === data5;
if (passed) passedCount++;
const details = `密钥: ${pinCode5}\n明文长度: ${data5.length} 字符\n密文长度: ${encrypted5.length} 字符\n解密后长度: ${decrypted5.length} 字符\n解密后与明文一致: ${passed}`;
addTestResult(`测试${testCount} - 长文本测试`, details, passed);
} catch (error) {
addTestResult(`测试${testCount} - 长文本测试`, error.message, false);
}
// 测试6: 错误密钥测试
testCount++;
try {
const pinCode6 = '260880';
const wrongKey = 'wrongkey';
const data6 = '123';
const encrypted6 = AESCrypto.encrypt(pinCode6, data6);
// 使用错误密钥解密,应该失败
const decrypted6 = AESCrypto.decrypt(wrongKey, encrypted6);
// 这里应该抛出错误,如果没有抛出则测试失败
addTestResult(`测试${testCount} - 错误密钥测试`, '使用错误密钥解密时没有抛出预期的错误', false);
} catch (error) {
// 预期应该抛出错误,所以测试通过
passedCount++;
addTestResult(`测试${testCount} - 错误密钥测试`, `使用错误密钥解密时正确抛出错误: ${error.message}`, true);
}
// 显示测试总结
const summary = document.createElement('div');
summary.className = 'test-item';
summary.innerHTML = `
`;
document.getElementById('test-results').appendChild(summary);
console.log(`=== AESCrypto 测试结束 ===`);
console.log(`总测试数: ${testCount}`);
console.log(`通过测试: ${passedCount}`);
console.log(`失败测试: ${testCount - passedCount}`);
console.log(`通过率: ${((passedCount / testCount) * 100).toFixed(2)}%`);
}
// 页面加载完成后自动运行测试
window.addEventListener('load', function() {
runTests();
});
</script>
</body>
</html>
const crypto = require("crypto");
/**
* 加密解密工具类
*/
class CryptoUtil {
// 常量定义
static get ALGORITHM() { return 'aes-256-cbc'; }
static get SALT_LENGTH() { return 16; }
static get IV_LENGTH() { return 16; }
static get KEY_LENGTH() { return 32; }
static get ITERATIONS() { return 100; }
static get HASH_ALGORITHM() { return 'sha256'; }
/**
* 安全的密钥派生函数
* @param {string|Buffer} pinCode - 密码或密钥
* @param {Buffer} salt - 盐值
* @returns {Promise<Buffer>} 派生的密钥
*/
static async deriveKeySecure(pinCode, salt) {
return new Promise((resolve, reject) => {
crypto.pbkdf2(
pinCode,
salt,
CryptoUtil.ITERATIONS,
CryptoUtil.KEY_LENGTH,
CryptoUtil.HASH_ALGORITHM,
(err, derivedKey) => {
if (err) return reject(err);
resolve(derivedKey);
}
);
});
}
/**
* PKCS7填充
* @param {Buffer} data - 要填充的数据
* @param {number} blockSize - 块大小
* @returns {Buffer} 填充后的数据
*/
static pkcs7Pad(data, blockSize = 16) {
const padding = blockSize - (data.length % blockSize);
const padded = Buffer.alloc(data.length + padding);
data.copy(padded);
padded.fill(padding, data.length);
return padded;
}
/**
* PKCS7取消填充
* @param {Buffer} padded - 填充后的数据
* @returns {Buffer} 取消填充后的数据
* @throws {Error} 无效的填充
*/
static pkcs7Unpad(padded) {
if (!Buffer.isBuffer(padded)) {
throw new TypeError('Input must be a Buffer');
}
const length = padded.length;
if (length === 0) {
throw new Error('Invalid padding: empty input');
}
const padding = padded[length - 1];
if (padding < 1 || padding > length) {
throw new Error(`Invalid padding: padding value ${padding} out of range`);
}
// 验证所有填充字节都正确
for (let i = length - padding; i < length; i++) {
if (padded[i] !== padding) {
throw new Error('Invalid padding: inconsistent padding bytes');
}
}
return padded.slice(0, length - padding);
}
/**
* 加密数据
* @param {string|Buffer} pinCode - 密码或密钥
* @param {string|Buffer} data - 要加密的数据
* @returns {Promise<string>} 加密后的Base64字符串
* @throws {TypeError} 无效的输入类型
* @throws {Error} 加密失败
*/
static async encrypt(pinCode, data) {
// 参数验证
if (typeof pinCode !== 'string' && !Buffer.isBuffer(pinCode)) {
throw new TypeError('pinCode must be a string or Buffer');
}
if (typeof data !== 'string' && !Buffer.isBuffer(data)) {
throw new TypeError('data must be a string or Buffer');
}
try {
// 生成随机盐和IV
const salt = await CryptoUtil.generateRandomBytes(CryptoUtil.SALT_LENGTH);
const iv = await CryptoUtil.generateRandomBytes(CryptoUtil.IV_LENGTH);
// 使用PBKDF2派生密钥
const key = await CryptoUtil.deriveKeySecure(pinCode, salt);
// 转换数据为Buffer
const dataBuffer = typeof data === 'string' ? Buffer.from(data, 'utf8') : data;
// PKCS7填充
const paddedData = CryptoUtil.pkcs7Pad(dataBuffer, 16);
// AES-256-CBC加密
const cipher = crypto.createCipheriv(CryptoUtil.ALGORITHM, key, iv);
cipher.setAutoPadding(false); // 禁用自动填充
const ciphertext = Buffer.concat([cipher.update(paddedData), cipher.final()]);
// 拼接盐、IV和密文
const result = Buffer.concat([salt, iv, ciphertext]);
return result.toString('base64');
} catch (error) {
throw new Error(`Encryption failed: ${error.message}`);
}
}
/**
* 解密数据
* @param {string|Buffer} pinCode - 密码或密钥
* @param {string} encryptedBase64 - 加密后的Base64字符串
* @returns {Promise<string>} 解密后的字符串
* @throws {TypeError} 无效的输入类型
* @throws {Error} 解密失败
*/
static async decrypt(pinCode, encryptedBase64) {
// 参数验证
if (typeof pinCode !== 'string' && !Buffer.isBuffer(pinCode)) {
throw new TypeError('pinCode must be a string or Buffer');
}
if (typeof encryptedBase64 !== 'string') {
throw new TypeError('encryptedBase64 must be a string');
}
try {
// Base64解码
const encryptedData = Buffer.from(encryptedBase64, 'base64');
// 验证数据长度(至少包含盐+IV)
const minLength = CryptoUtil.SALT_LENGTH + CryptoUtil.IV_LENGTH;
if (encryptedData.length < minLength) {
throw new Error(`Encrypted data too short: expected at least ${minLength} bytes, got ${encryptedData.length} bytes`);
}
// 提取盐、IV和密文
const salt = encryptedData.slice(0, CryptoUtil.SALT_LENGTH);
const iv = encryptedData.slice(CryptoUtil.SALT_LENGTH, CryptoUtil.SALT_LENGTH + CryptoUtil.IV_LENGTH);
const ciphertext = encryptedData.slice(CryptoUtil.SALT_LENGTH + CryptoUtil.IV_LENGTH);
// 验证密文长度
if (ciphertext.length % 16 !== 0) {
throw new Error('Invalid ciphertext length: must be a multiple of 16 bytes');
}
// 使用PBKDF2派生密钥
const key = await CryptoUtil.deriveKeySecure(pinCode, salt);
// AES-256-CBC解密
const decipher = crypto.createDecipheriv(CryptoUtil.ALGORITHM, key, iv);
decipher.setAutoPadding(false); // 禁用自动填充
const decrypted = Buffer.concat([
decipher.update(ciphertext),
decipher.final(),
]);
// 去除填充
const unpadded = CryptoUtil.pkcs7Unpad(decrypted);
return unpadded.toString('utf8');
} catch (error) {
throw new Error(`Decryption failed: ${error.message}`);
}
}
/**
* 生成随机字节
* @param {number} length - 字节长度
* @returns {Promise<Buffer>} 随机字节
*/
static async generateRandomBytes(length) {
return new Promise((resolve, reject) => {
crypto.randomBytes(length, (err, bytes) => {
if (err) return reject(err);
resolve(bytes);
});
});
}
/**
* 生成随机字符串
* @param {number} length - 字符串长度
* @returns {Promise<string>} 随机字符串
*/
static async generateRandomString(length) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
// 生成安全的随机字节
const randomBytes = await CryptoUtil.generateRandomBytes(length);
for (let i = 0; i < length; i++) {
// 使用模62确保均匀分布到62个字符
const index = randomBytes[i] % chars.length;
result += chars[index];
}
return result;
}
}
// 测试函数
async function runTests() {
try {
// 测试pinCode1
const pinCode1 = '260880';
// JSON 数据
const data1 = JSON.stringify({
"header": {
"action": "TransactionStatus",
"requestID": "18fd2b62-6f65-40f2-8b94-88ef32f07a3f",
"clientDeviceSN": "126498561093",
"timestamp": "2025-11-12T10:11:04+00:00"
},
"body": {
"targetReferenceID": "f8b13b22-16ca-4a87-95a4-df4bebf09ee1"
}
});
console.log('pinCode1:', pinCode1);
const encryptedData1 = await CryptoUtil.encrypt(pinCode1, data1);
console.log('Encrypted Data 1:', encryptedData1);
const decryptedData1 = await CryptoUtil.decrypt(pinCode1, encryptedData1);
console.log('Decrypted Data 1:', decryptedData1);
console.log('Decrypted Data 1 is valid JSON:', JSON.parse(decryptedData1));
// 测试pinCode2
const pinCode2 = 'OvSdpyD2FoUNR5rNyte41QqZzR1Y4DVN';
console.log('pinCode2:', pinCode2);
const encryptedData2 = await CryptoUtil.encrypt(pinCode2, data1);
console.log('Encrypted Data 2:', encryptedData2);
const decryptedData2 = await CryptoUtil.decrypt(pinCode2, encryptedData2);
console.log('Decrypted Data 2:', decryptedData2);
console.log('Decrypted Data 2 is valid JSON:', JSON.parse(decryptedData2));
// 测试随机字符串生成
const randomString = await CryptoUtil.generateRandomString(32);
console.log('Generated Random String:', randomString);
// 测试使用随机字符串作为密钥
const encryptedData3 = await CryptoUtil.encrypt(randomString, data1);
const decryptedData3 = await CryptoUtil.decrypt(randomString, encryptedData3);
console.log('Random key encryption/decryption works:', JSON.parse(decryptedData3).header.action === 'TransactionStatus');
} catch (error) {
console.error('Test failed:', error.message);
console.error(error.stack);
}
}
// 运行测试
runTests();
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import org.json.JSONObject;
/**
* AES加密解密工具类,使用AES-256-CBC模式和PBKDF2密钥派生
*/
public class AESCryptoUtil {
// 加密相关常量
private static final int SALT_LENGTH = 16; // 盐的长度(字节)
private static final int IV_LENGTH = 16; // IV的长度(字节)
private static final int KEY_LENGTH = 32; // AES-256密钥长度(字节)
private static final int ITERATIONS = 100; // PBKDF2迭代次数
private static final String ALGORITHM = "AES/CBC/PKCS5Padding"; // 加密算法(PKCS5Padding在Java中实际是PKCS7)
private static final String KEY_DERIVATION_ALGORITHM = "PBKDF2WithHmacSHA256"; // 密钥派生算法
private static final String AES_ALGORITHM = "AES"; // AES算法名称
// 线程安全的SecureRandom实例
private static final SecureRandom SECURE_RANDOM;
static {
try {
SECURE_RANDOM = SecureRandom.getInstanceStrong();
} catch (NoSuchAlgorithmException e) {
// 如果强随机数生成器不可用,回退到默认实现
throw new RuntimeException("Failed to initialize SecureRandom", e);
}
}
/**
* 使用PBKDF2算法从密码和盐派生AES密钥
*
* @param pinCode 密码或PIN码
* @param salt 盐值
* @return 派生的AES密钥
* @throws NoSuchAlgorithmException 如果算法不可用
* @throws InvalidKeySpecException 如果密钥规范无效
* @throws IllegalArgumentException 如果输入参数无效
*/
private static SecretKey deriveKeySecure(String pinCode, byte[] salt)
throws NoSuchAlgorithmException, InvalidKeySpecException {
if (pinCode == null || pinCode.isEmpty()) {
throw new IllegalArgumentException("Pin code cannot be null or empty");
}
if (salt == null || salt.length != SALT_LENGTH) {
throw new IllegalArgumentException("Invalid salt");
}
SecretKeyFactory factory = SecretKeyFactory.getInstance(KEY_DERIVATION_ALGORITHM);
KeySpec spec = new PBEKeySpec(pinCode.toCharArray(), salt, ITERATIONS, KEY_LENGTH * 8);
SecretKey tmp = factory.generateSecret(spec);
return new SecretKeySpec(tmp.getEncoded(), AES_ALGORITHM);
}
/**
* 加密数据
*
* @param pinCode 加密密钥(用于派生AES密钥)
* @param data 要加密的数据
* @return Base64编码的加密数据(包含盐、IV和密文)
* @throws Exception 如果加密过程中发生错误
* @throws IllegalArgumentException 如果输入参数无效
*/
public static String encrypt(String pinCode, String data) throws Exception {
if (pinCode == null || pinCode.isEmpty()) {
throw new IllegalArgumentException("Pin code cannot be null or empty");
}
if (data == null) {
throw new IllegalArgumentException("Data cannot be null");
}
// 生成随机盐
byte[] salt = new byte[SALT_LENGTH];
SECURE_RANDOM.nextBytes(salt);
// 生成随机IV
byte[] iv = new byte[IV_LENGTH];
SECURE_RANDOM.nextBytes(iv);
// 使用PBKDF2派生密钥
SecretKey key = deriveKeySecure(pinCode, salt);
// 创建AES-256-CBC加密器
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
// 加密数据
byte[] ciphertext = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
// 拼接盐、IV和密文
byte[] result = new byte[salt.length + iv.length + ciphertext.length];
System.arraycopy(salt, 0, result, 0, salt.length);
System.arraycopy(iv, 0, result, salt.length, iv.length);
System.arraycopy(ciphertext, 0, result, salt.length + iv.length, ciphertext.length);
// Base64编码
return Base64.getEncoder().encodeToString(result);
}
/**
* 解密数据
*
* @param pinCode 解密密钥(用于派生AES密钥)
* @param encryptedBase64 Base64编码的加密数据
* @return 解密后的数据
* @throws Exception 如果解密过程中发生错误
* @throws IllegalArgumentException 如果输入参数无效
*/
public static String decrypt(String pinCode, String encryptedBase64) throws Exception {
if (pinCode == null || pinCode.isEmpty()) {
throw new IllegalArgumentException("Pin code cannot be null or empty");
}
if (encryptedBase64 == null || encryptedBase64.isEmpty()) {
throw new IllegalArgumentException("Encrypted data cannot be null or empty");
}
// Base64解码
byte[] encryptedData = Base64.getDecoder().decode(encryptedBase64);
// 验证数据长度(至少包含盐+IV)
if (encryptedData.length < SALT_LENGTH + IV_LENGTH) {
throw new IllegalArgumentException("Encrypted data too short");
}
// 提取盐、IV和密文
byte[] salt = new byte[SALT_LENGTH];
byte[] iv = new byte[IV_LENGTH];
byte[] ciphertext = new byte[encryptedData.length - SALT_LENGTH - IV_LENGTH];
System.arraycopy(encryptedData, 0, salt, 0, SALT_LENGTH);
System.arraycopy(encryptedData, SALT_LENGTH, iv, 0, IV_LENGTH);
System.arraycopy(encryptedData, SALT_LENGTH + IV_LENGTH, ciphertext, 0, ciphertext.length);
// 使用PBKDF2派生密钥
SecretKey key = deriveKeySecure(pinCode, salt);
// 创建AES-256-CBC解密器
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
// 解密数据
byte[] decrypted = cipher.doFinal(ciphertext);
return new String(decrypted, StandardCharsets.UTF_8);
}
/**
* 生成指定长度的随机字符串
*
* @param length 字符串长度
* @return 随机字符串
* @throws IllegalArgumentException 如果长度小于等于0
*/
public static String generateRandomString(int length) {
if (length <= 0) {
throw new IllegalArgumentException("Length must be positive");
}
final String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
StringBuilder result = new StringBuilder(length);
for (int i = 0; i < length; i++) {
int index = SECURE_RANDOM.nextInt(chars.length());
result.append(chars.charAt(index));
}
return result.toString();
}
/**
* 测试加密解密功能
*/
public static void main(String[] args) {
try {
// 测试pinCode1
String pinCode1 = "260880";
// 创建测试JSON数据
Map<String, Object> header = new HashMap<>();
header.put("requestID", "ff467f02-5b69-45f3-81aa-bffcca55fe80");
header.put("clientDeviceSN", "126498561093");
header.put("timestamp", "2025-11-12T10:11:04+00:00");
Map<String, Object> body = new HashMap<>();
body.put("pairUuid", "8daf4dc0-6ad6-44b1-8556-a0f1549c0fc9");
Map<String, Object> dataMap = new HashMap<>();
dataMap.put("header", header);
dataMap.put("body", body);
JSONObject jsonData = new JSONObject(dataMap);
String data1 = jsonData.toString();
System.out.println("pinCode1: " + pinCode1);
System.out.println("Original Data: " + data1);
// 加密
String encryptedData1 = encrypt(pinCode1, data1);
System.out.println("Encrypted Data 1: " + encryptedData1);
// 解密
String decryptedData1 = decrypt(pinCode1, encryptedData1);
System.out.println("Decrypted Data 1: " + decryptedData1);
// 验证
if (decryptedData1.equals(data1)) {
System.out.println("✓ Test 1: Encryption/Decryption successful");
} else {
System.out.println("✗ Test 1: Encryption/Decryption failed");
}
// 测试pinCode2
String pinCode2 = "OvSdpyD2FoUNR5rNyte41QqZzR1Y4DVN";
System.out.println("\npinCode2: " + pinCode2);
// 使用相同的测试数据
String encryptedData2 = encrypt(pinCode2, data1);
System.out.println("Encrypted Data 2: " + encryptedData2);
// 解密
String decryptedData2 = decrypt(pinCode2, encryptedData2);
System.out.println("Decrypted Data 2: " + decryptedData2);
// 验证
if (decryptedData2.equals(data1)) {
System.out.println("✓ Test 2: Encryption/Decryption successful");
} else {
System.out.println("✗ Test 2: Encryption/Decryption failed");
}
// 错误密钥测试
System.out.println("\n--- Additional Tests ---");
String wrongKey = "wrong_password";
try {
decrypt(wrongKey, encryptedData1);
System.out.println("✗ Wrong key test: Decryption should have failed but didn't");
} catch (Exception e) {
System.out.println("✓ Wrong key test: Decryption correctly failed");
}
// 测试生成随机字符串
String randomStr = generateRandomString(32);
System.out.println("\nGenerated random string (32 chars): " + randomStr);
// 测试使用随机字符串作为密钥
System.out.println("\nRandom key: " + randomStr);
String testMessage = "This is a test message";
String encryptedData3 = encrypt(randomStr, testMessage);
String decryptedData3 = decrypt(randomStr, encryptedData3);
if (decryptedData3.equals(testMessage)) {
System.out.println("✓ Random key test: Encryption/Decryption successful");
} else {
System.out.println("✗ Random key test: Encryption/Decryption failed");
}
} catch (Exception e) {
System.err.println("Test failed with exception: " + e.getMessage());
e.printStackTrace();
}
}
}
package main
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"testing"
"golang.org/x/crypto/pbkdf2"
)
// AESCrypto 提供AES-256-CBC加密解密功能
type AESCrypto struct {
iterations int
}
// NewAESCrypto 创建一个新的AESCrypto实例
func NewAESCrypto(iterations int) *AESCrypto {
if iterations <= 0 {
iterations = 100 // 默认迭代次数
}
return &AESCrypto{
iterations: iterations,
}
}
// 常量定义
const (
SaltSize = 16 // 盐的大小(字节)
IVSize = 16 // IV的大小(字节)
KeySize = 32 // 密钥的大小(字节,AES-256需要32字节)
BlockSize = aes.BlockSize
CharsSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
CharsSetSize = len(CharsSet)
)
// deriveKey 使用PBKDF2算法从pinCode和salt派生密钥
func (a *AESCrypto) deriveKey(pinCode string, salt []byte) []byte {
return pbkdf2.Key([]byte(pinCode), salt, a.iterations, KeySize, sha256.New)
}
// pkcs7Pad 对数据进行PKCS7填充
func pkcs7Pad(data []byte, blockSize int) []byte {
padding := blockSize - (len(data) % blockSize)
padText := bytes.Repeat([]byte{byte(padding)}, padding)
return append(data, padText...)
}
// pkcs7Unpad 去除PKCS7填充
func pkcs7Unpad(data []byte) ([]byte, error) {
if len(data) == 0 {
return nil, errors.New("empty data")
}
padding := int(data[len(data)-1])
if padding < 1 || padding > len(data) {
return nil, errors.New("invalid padding")
}
// 验证所有填充字节都正确
for i := len(data) - padding; i < len(data); i++ {
if data[i] != byte(padding) {
return nil, errors.New("invalid padding")
}
}
return data[:len(data)-padding], nil
}
// Encrypt 使用AES-256-CBC算法加密数据
func (a *AESCrypto) Encrypt(pinCode, data string) (string, error) {
if pinCode == "" {
return "", errors.New("pinCode is empty")
}
// 生成随机盐
salt := make([]byte, SaltSize)
if _, err := rand.Read(salt); err != nil {
return "", fmt.Errorf("failed to generate salt: %w", err)
}
// 生成随机IV
iv := make([]byte, IVSize)
if _, err := rand.Read(iv); err != nil {
return "", fmt.Errorf("failed to generate IV: %w", err)
}
// 使用PBKDF2派生密钥
key := a.deriveKey(pinCode, salt)
// 创建AES-256-CBC加密器
block, err := aes.NewCipher(key)
if err != nil {
return "", fmt.Errorf("failed to create cipher: %w", err)
}
// PKCS7填充
paddedData := pkcs7Pad([]byte(data), block.BlockSize())
// 加密
ciphertext := make([]byte, len(paddedData))
mode := cipher.NewCBCEncrypter(block, iv)
mode.CryptBlocks(ciphertext, paddedData)
// 拼接盐、IV和密文
result := make([]byte, 0, SaltSize+IVSize+len(ciphertext))
result = append(result, salt...)
result = append(result, iv...)
result = append(result, ciphertext...)
// Base64编码
return base64.StdEncoding.EncodeToString(result), nil
}
// Decrypt 使用AES-256-CBC算法解密数据
func (a *AESCrypto) Decrypt(pinCode, encryptedBase64 string) (string, error) {
if pinCode == "" {
return "", errors.New("pinCode is empty")
}
if encryptedBase64 == "" {
return "", errors.New("encrypted data is empty")
}
// Base64解码
encryptedData, err := base64.StdEncoding.DecodeString(encryptedBase64)
if err != nil {
return "", fmt.Errorf("failed to decode base64: %w", err)
}
// 验证数据长度(至少包含盐+IV)
if len(encryptedData) < SaltSize+IVSize {
return "", errors.New("encrypted data too short")
}
// 提取盐、IV和密文
salt := encryptedData[:SaltSize]
iv := encryptedData[SaltSize : SaltSize+IVSize]
ciphertext := encryptedData[SaltSize+IVSize:]
// 验证密文长度是否为块大小的倍数
if len(ciphertext)%BlockSize != 0 {
return "", errors.New("invalid ciphertext length")
}
// 使用PBKDF2派生密钥
key := a.deriveKey(pinCode, salt)
// 创建AES-256-CBC解密器
block, err := aes.NewCipher(key)
if err != nil {
return "", fmt.Errorf("failed to create cipher: %w", err)
}
// 解密
decrypted := make([]byte, len(ciphertext))
mode := cipher.NewCBCDecrypter(block, iv)
mode.CryptBlocks(decrypted, ciphertext)
// 去除填充
unpadded, err := pkcs7Unpad(decrypted)
if err != nil {
return "", fmt.Errorf("failed to unpad: %w", err)
}
return string(unpadded), nil
}
// GenerateRandomStringSimple 生成指定长度的随机字符串
func GenerateRandomStringSimple(length int) (string, error) {
if length <= 0 {
return "", errors.New("length must be positive")
}
result := make([]byte, length)
// 为了获得均匀分布,需要生成足够的随机字节
// 每个随机字节有256种可能,62个字符,需要拒绝采样
// 但为了简单高效,我们可以一次生成足够的字节
// 使用约1.2倍长度来减少重试次数
bufSize := int(float64(length) * 1.2)
buf := make([]byte, bufSize)
for i := 0; i < length; {
// 生成随机字节
if _, err := rand.Read(buf); err != nil {
return "", fmt.Errorf("failed to generate random bytes: %w", err)
}
// 处理每个随机字节
for _, b := range buf {
// 使用拒绝采样确保均匀分布
// 62 * 4 = 248,我们只接受0-247的值
if b < 248 {
// 将0-247映射到0-61,确保均匀分布
idx := int(b) % CharsSetSize
result[i] = CharsSet[idx]
i++
if i == length {
break
}
}
}
}
return string(result), nil
}
// 测试数据结构
type Header struct {
RequestID string `json:"requestID"`
ClientDeviceSN string `json:"clientDeviceSN"`
Timestamp string `json:"timestamp"`
}
type Body struct {
PairUuid string `json:"pairUuid"`
}
type TestData struct {
Header Header `json:"header"`
Body Body `json:"body"`
}
func main(t *testing.T) {
// 创建加密解密实例
crypto := NewAESCrypto(100)
// 测试pinCode1
pinCode1 := "260880"
// 创建测试数据
testData := TestData{
Header: Header{
RequestID: "ff467f02-5b69-45f3-81aa-bffcca55fe80",
ClientDeviceSN: "126498561093",
Timestamp: "2025-11-12T10:11:04+00:00",
},
Body: Body{
PairUuid: "8daf4dc0-6ad6-44b1-8556-a0f1549c0fc9",
},
}
// 将数据转换为JSON字符串
data1, err := json.Marshal(testData)
if err != nil {
fmt.Printf("Failed to marshal JSON: %v\n", err)
return
}
fmt.Printf("pinCode1: %s\n", pinCode1)
// 加密
encryptedData1, err := crypto.Encrypt(pinCode1, string(data1))
if err != nil {
fmt.Printf("Encryption failed: %v\n", err)
return
}
fmt.Printf("Encrypted Data 1: %s\n", encryptedData1)
// 解密
decryptedData1, err := crypto.Decrypt(pinCode1, encryptedData1)
if err != nil {
fmt.Printf("Decryption failed: %v\n", err)
return
}
fmt.Printf("Decrypted Data 1: %s\n", decryptedData1)
// 验证解密后的数据是否与原始数据相同
if decryptedData1 == string(data1) {
fmt.Println("✓ Test 1: Encryption/Decryption successful")
} else {
fmt.Println("✗ Test 1: Encryption/Decryption failed")
}
// 测试pinCode2
// pinCode2, err := GenerateRandomStringSimple(32)
// if err != nil {
// fmt.Printf("Failed to generate random string: %v\n", err)
// return
// }
pinCode2 := "OvSdpyD2FoUNR5rNyte41QqZzR1Y4DVN"
fmt.Printf("\npinCode2: %s\n", pinCode2)
// 使用相同的测试数据
encryptedData2, err := crypto.Encrypt(pinCode2, string(data1))
if err != nil {
fmt.Printf("Encryption failed: %v\n", err)
return
}
fmt.Printf("Encrypted Data 2: %s\n", encryptedData2)
// 解密
decryptedData2, err := crypto.Decrypt(pinCode2, encryptedData2)
if err != nil {
fmt.Printf("Decryption failed: %v\n", err)
return
}
fmt.Printf("Decrypted Data 2: %s\n", decryptedData2)
// 验证解密后的数据是否与原始数据相同
if decryptedData2 == string(data1) {
fmt.Println("✓ Test 2: Encryption/Decryption successful")
} else {
fmt.Println("✗ Test 2: Encryption/Decryption failed")
}
// 额外测试:错误密钥测试
fmt.Println("\n--- Additional Tests ---")
// 测试错误密钥
wrongKey := "wrong_password"
_, err = crypto.Decrypt(wrongKey, encryptedData1)
if err != nil {
fmt.Printf("✓ Wrong key test: Decryption correctly failed with error: %v\n", err)
} else {
fmt.Println("✗ Wrong key test: Decryption should have failed but didn't")
}
// 测试损坏的数据
corruptedData := encryptedData1[:len(encryptedData1)-10] + "abc"
_, err = crypto.Decrypt(pinCode1, corruptedData)
if err != nil {
fmt.Printf("✓ Corrupted data test: Decryption correctly failed with error: %v\n", err)
} else {
fmt.Println("✗ Corrupted data test: Decryption should have failed but didn't")
}
// 测试生成随机字符串
randomStr, err := GenerateRandomStringSimple(32)
if err != nil {
fmt.Printf("Failed to generate random string: %v\n", err)
return
}
fmt.Printf("\nGenerated random string (32 chars): %s\n", randomStr)
// 测试使用随机字符串作为密钥
fmt.Printf("\nRandom key: %s\n", randomStr)
encryptedData3, err := crypto.Encrypt(randomStr, "This is a test message")
if err != nil {
fmt.Printf("Encryption with random key failed: %v\n", err)
return
}
decryptedData3, err := crypto.Decrypt(randomStr, encryptedData3)
if err != nil {
fmt.Printf("Decryption with random key failed: %v\n", err)
return
}
if decryptedData3 == "This is a test message" {
fmt.Println("✓ Random key test: Encryption/Decryption successful")
} else {
fmt.Println("✗ Random key test: Encryption/Decryption failed")
}
}
2. Request and Response Protocol Data Format
2.1 Request header
| Variable | Type | Required | Description |
|---|---|---|---|
| x-p-business-id | String | Y | The current business ID, Specify the ECR device to receive orders for a certain Business |
| x-device-sn | String | Y | The serial number of the ecr terminal payment device |
| x-request-id | String | Y | Every time communication takes place, it is crucial to ensure that the Request ID is a unique UUID. |
2.2 Request data structure
Request JSON
| Variable | Type | Required | Description |
|---|---|---|---|
| version | String | Y | Current communication protocol version number |
| action | enum | Y | Actions: Pair, DeviceInfo, Sale, ... |
| data | String | Y | Encrypted data |
Request "data" structure
| Variable | Type | Required | Description |
|---|---|---|---|
| header | Object | Y | Protocol header |
| body | Object | N | Protocol body |
Request "data.header" structure
| Variable | Type | Required | Description |
|---|---|---|---|
| requestID | UUID | Y | Request ID: Every time communication takes place, it is crucial to ensure that the Request ID is a unique UUID. In the event of a response, the corresponding identifier will be returned in the response header as the "responseID". |
| clientDeviceSN | String | Y | Client device serial number |
| timestamp | datetime | Y | The timestamp of the current request |
Example:
/*
Encrypt the original data:
{
"header": {
"requestID": "9c07d8d7-2a43-4a29-9c6d-6b8d8f7d44e5",
"clientDeviceSN": "126498561093",
"timestamp": "2025-11-12T10:11:04+00:00"
},
"body": {
"pairUuid": "3f37e6c0-bf6e-4c00-b1fa-b2bd5e1d6a3b"
}
}
*/
{
"version": "2.0",
"action": "Pair",
"data": "WJq3v/ZX/aMMR45WFqfs9BZ2J93EtSTYQh4JOD17ldQkzXi6ohsjxCdl4mpvAOqhFyWPVuEadUubmRzfACUAQpqledyJhOdtQTMlr4KnZXBJw6g3TOFTeKe1I/DqMiEEG0X9BEelUNS+0+FpaoWN1mVkuKEpt8XMGrHyA+M4XbaMwjQ3fMeoR77FULPvM9MCkw6B3QTGONMml51X8pqXsBYTrcpS5SLFVvdL5e+1CcIuVvliyrV3MDd+cwlSjHNIoaLwDEgu9wDNjGTYe1L8Ww=="
}
2.3 Response data structure
Request JSON
| Variable | Type | Required | Description |
|---|---|---|---|
| version | String | Y | Current communication protocol version number |
| action | enum | Y | Actions: DeviceInfo, Sale, PreAuth, Abort, Void, Refund |
| data | String | Y | Encrypted data |
Response "data" structure
| Variable | Type | Required | Description |
|---|---|---|---|
| header | Object | Y | Protocol header |
| body | Object | N | Protocol body |
Response "data.header" structure
| Variable | Type | Required | Description |
|---|---|---|---|
| responseID | String | Y | The response ID in the response Header is derived from the request ID in the request header. |
| serverDeviceSN | String | Y | The serial number of the terminal payment device |
| timestamp | datetime | N | The timestamp of the current request |
Example:
/*
Encrypt the original data:
{
"header": {
"responseID": "9c07d8d7-2a43-4a29-9c6d-6b8d8f7d44e5",
"serverDeviceSN": "NEXGO-N96-1170270945",
"timestamp": "2025-11-12T10:12:04+00:00"
},
"body": {
"pairUuid": "3f37e6c0-bf6e-4c00-b1fa-b2bd5e1d6a3b",
"ackUuid": "4d366618-49cf-4f93-bd23-25388a573b0c"
}
}
*/
{
"version": 2.0,
"action": "Pair",
"data": "G6RaFsGPTiVzAYsUTu/gY1Igjv+4THb1tZ7vwXWP9f1CYRRrNKKuz4Op7ai9xCqKCdBymnO/YAQTB9egq10DIp/mxQ4pG1dGyL+Erkd91G4xYJd9S7iXNqnCwEn2wOhMIn3sP0H6vgTzoGX9ZOi59pLhuGqYTbHBPvDIkavEIweWWVQjEUdQn7HG0ymqxq4O36giOlh5NldSPwH5LjbnOamEm+Y41jIW/dRexnd7hhKkF/TrZg2lDYKOry9NflNzoMPHiB2/7YgVVmgVfBysW9dqqNRiJXxYLOD6UGoJlQqsICeiidKm6SYL4dkKHYniFEnZ0a5KGdxZs7viSahmiA=="
}
3. Interface Protocol
3.1 Pair
After the customer device acquires the 6-digit pairing code, it uses the "Pair" protocol for pairing. After the Wonder terminal receives a pairing request, it will return a UUID identifier, which will be used by the "Ack" protocol.
- Action: Pair
- Pin Code: pinCode1
Request "data.body" structure
| Variable | Type | Required | Description |
|---|---|---|---|
| pairUuid | UUID | Y | Pair the uuid identifier |
Example:
/*
Pin Code: pinCode1
Encrypt the original data:
{
"header": {
"requestID": "9c07d8d7-2a43-4a29-9c6d-6b8d8f7d44e5",
"clientDeviceSN": "126498561093",
"timestamp": "2025-11-12T10:11:04+00:00"
},
"body": {
"pairUuid": "3f37e6c0-bf6e-4c00-b1fa-b2bd5e1d6a3b"
}
}
*/
{
"version": "2.0",
"action": "Pair",
"data": "KhGmDIJZQXsNHrLM1hDB/kjPjlSpmUQVNIrRLEAptCHb+GHiqdZTnfDRWJVBJDzTyLtqDXLJYRR3433sIEm7hBvpI8vZ4WeTIxcp1BUd4GwRjutbUMCQ8vc5GTlu8aWvSoChYWeHXIu5MmWXbfkWe8AMAHLtnjuvrZfV17mTlvX+09hskf5zCcFKKXVq6qA24o4d5crbIoKEmRI0K94YSPo99N9mEdnAVdh5xNxQmzsp1JlKMnxQGIy2K1HuUmabW1JLWID/In4sADJDdRk2069oYrP1PWYUqKKoahtH7GNQqJ0Syux/IykdoXVZ2dmn"
}
Response "data.body" structure
| Variable | Type | Required | Description |
|---|---|---|---|
| pairUuid | UUID | Y | Pair the uuid identifier |
| ackUuid | UUID | Y | Ack the uuid identifier |
| pinCode | String | Y | In the test code, it is pinCode2, which is used in the Ack interface and the business interface |
Example:
/*
Pin Code: pinCode1
Encrypt the original data:
{
"header": {
"responseID": "9c07d8d7-2a43-4a29-9c6d-6b8d8f7d44e5",
"serverDeviceSN": "NEXGO-N96-1170270945",
"timestamp": "2025-11-12T10:12:04+00:00"
},
"body": {
"pairUuid": "3f37e6c0-bf6e-4c00-b1fa-b2bd5e1d6a3b",
"ackUuid": "4d366618-49cf-4f93-bd23-25388a573b0c",
"pinCode": "OvSdpyD2FoUNR5rNyte41QqZzR1Y4DVN"
}
}
*/
{
"version": 2.0,
"action": "Pair",
"data": "N7rbBZmxrGP5kh7vUaGOs1CslAJVsa53OKmEjdkx72ulWIptVPuJNCfz+GCIrt7E66S+nG5uMpNkj7XOtj2IppH/WwUQQMSBVsFc9meSy0+GjOARJ61W9CXxVu2sBMUNWGgzuxd1bhGmXD/GNTHLuuzFZJv0ANIR26tf+MyUs19a/cYxhHba8tgcIYQG/FLEHK7hI2p3hX0ETESxEDDyogVP0a8F050Jua5nIjU/BBfhfC1g0/Iu5q+oSN/caZcZyO6F/a5dF6EXPDbJY0yr8QDsH8nr8mEfaO3HS7gtozcVIylsmbXNftAzlj4Ex1768VeVHXVvfimjUzGy4HsvxiJBAQixAkKR/nQVguyCSWojmOXwDFm5jjjmoXTSvZxsrYuogke4gAlW055leR9fGiHesXoPL1zyzWSR1EeuxtAGZB1V2B9yUKjRhc5GPTWH"
}
3.2 Ack
After the client terminal initiates the pairing, it will receive an "ackUuid" identifier. This protocol needs to carry the "ackUuid" identifier to confirm that the pairing has been successful.
- Action: Ack
- Pin Code: pinCode2
Request "data.body" structure
| Variable | Type | Required | Description |
|---|---|---|---|
| ackUuid | UUID | Y | The Wonder device will use this UUID to confirm whether the pairing was successful |
Example:
/*
Pin Code: pinCode2 (OvSdpyD2FoUNR5rNyte41QqZzR1Y4DVN)
Encrypt the original data:
{
"header": {
"requestID": "73a95cfe-b0f1-42b3-82d1-5a640f61e359",
"clientDeviceSN": "126498561093",
"timestamp": "2025-11-12T10:11:04+00:00"
},
"body": {
"ackUuid": "4d366618-49cf-4f93-bd23-25388a573b0c"
}
}
*/
{
"version": "2.0",
"action": "Ack",
"data": "1Rlzdz9YVYhK9cI0tqodWqay2D8t0vs3slnvVn0lW5G2EqFcfQtC/4j7oQ+X/MOvADkbVGVDREB54LxJWK5UtzNVx1+z5R/ydgYryjcUuI4p724nsggY0lIbIBE+6yCDZQE6iXYZYAVAUkdyukNB18WbmqEK0+V+uPt/NWc4H0t5drhK+nz8uu62h4PT/Fxma5/ZcnBbRqjSMe1nUJmlqY0vKmJhhZ17N9M85rgh/JUsMebNMmERR6XDbnuls4bKY4hffSVzNbWHjBWjdfX60KU2r7SBE8HiEKS41FcVDhl2dmVcNybz/+RTSxMsnEvA"
}
Response "data.body" structure
| Variable | Type | Required | Description |
|---|---|---|---|
| deviceStatus | String | Y | Payment device transaction status: Free / Busy Free: The current device is idle and available for new transactions; Busy: The current device is occupied with an ongoing transaction and is not available for other transactions at the moment. |
| networkStatus | String | Y | The current network connection status of the payment device: Connected / Disconnected |
| softwareVersion | String | Y | The software version of the payment device |
| businessID | String | Y | The response ID in the response Header is derived from the request ID in the request header. |
Example:
/*
Pin Code: pinCode2 (OvSdpyD2FoUNR5rNyte41QqZzR1Y4DVN)
Encrypt the original data:
{
"header": {
"responseID": "73a95cfe-b0f1-42b3-82d1-5a640f61e359",
"serverDeviceSN": "NEXGO-N96-1170270945",
"timestamp": "2025-11-12T10:12:04+00:00"
},
"body": {
"deviceStatus": "Free",
"networkStatus": "Connected",
"softwareVersion": "1.0.0(188)",
"businessID": "ff467f02-5b69-45f3-81aa-bffcca55fe8f"
}
}
*/
{
"version": 2.0,
"action": "Ack",
"data": "thygIk5FDezl1xaeyOFkwduPBRl8PS65YQ5+aa1bLCT6gjIVGbjGsQlvkOl+24To6ztvk3CtJ5Jgeimy93RuW3cGYflth+Eb8A2031pbUa8918aJ08lHhVmbeajnWoaN9iWmExBuqkN4EA76SB6ELELwJQR3z0xlfREuhDyhlhIeBTs5dy+UptkKzIpHzcMB6aPL37qF+TIAW6M4B7hLniTM5RfCmFwQjDRNrj78pwA8teLrvPrGSXceQnbMxlk2BXl1BhVnhTSxVh8iP/KtLi9MxO4YvSfBsrfOkwrFLNIh3SardG0V2QVgGwdfKxmCSIf4GxuDF3bJz8XM8CowAogC0dY5zea4Ms2ifblCI13dD/uMJF3qrS36Xuj+LN7lO3UYWYtNzBy0vYo45PrZ/5Zh4WOeZ5sZk43Ycotj+fk="
}
3.3 Device Info
- Action: DeviceInfo
- Pin Code: pinCode2
Request "data.body" structure
This request has no body
Example:
/*
Pin Code: pinCode2 (OvSdpyD2FoUNR5rNyte41QqZzR1Y4DVN)
Encrypt the original data:
{
"header": {
"requestID": "2c7f32e1-b9e4-4b34-96cc-15c51289f69b",
"clientDeviceSN": "126498561093",
"timestamp": "2025-11-12T10:11:04+00:00"
}
}
*/
{
"version": "2.0",
"action": "DeviceInfo",
"data": "fZL3Z4ZlYRz5dt5ls0orrG5Pw1vdHxK+35tOc1aDIyInrHErmJRfeohzuviFFb7FuaxMZYPKU2GEMVIq8BvS3ua2xAj1CWHnduaO0YGTHfcuYTOTZxy3CZHvMtphPux+LZ2n3QlkhRPXeMwDlcOeaq1lKLerkcQf1SDSK14MvrTVg/NuB3BArPVmZsX9zBZ1pDrc7ixr4O/yNLD6RR/9JOKt3XWYqAJyztN06hYfSSI="
}
Response "data.body" structure
| Variable | Type | Required | Description |
|---|---|---|---|
| deviceStatus | String | Y | Payment device transaction status: Free / Busy Free: The current device is idle and available for new transactions; Busy: The current device is occupied with an ongoing transaction and is not available for other transactions at the moment. |
| networkStatus | String | Y | The current network connection status of the payment device: Connected / Disconnected |
| softwareVersion | String | Y | The software version of the payment device |
| businessID | String | Y | The response ID in the response Header is derived from the request ID in the request header. |
Example:
/*
Pin Code: pinCode2 (OvSdpyD2FoUNR5rNyte41QqZzR1Y4DVN)
Encrypt the original data:
{
"header": {
"responseID": "2c7f32e1-b9e4-4b34-96cc-15c51289f69b",
"serverDeviceSN": "NEXGO-N96-1170270945",
"timestamp": "2025-11-12T10:12:04+00:00"
},
"body": {
"deviceStatus": "Free",
"networkStatus": "Connected",
"softwareVersion": "1.0.0(188)",
"businessID": "ff467f02-5b69-45f3-81aa-bffcca55fe8f"
}
}
*/
{
"version": "2.0",
"action": "DeviceInfo",
"data": "+AGm65vmYsIBpb0oOlvyCkcJ/BurpmzLL7LusZW2mlfmEE3bYAD8hbwbrOshjozgFtkL+oMSrSfvs8rxHxdr5RU9WVCBgfognPaJj+9XnNJ43vnY7cjr5LvN5OZg8HNEiRYRQipGkjlPqiqWpyYCHs1nSIFoR/KSUvIokFKDY5USySXSBs5cp+vdbO6THLjWPYrgywQ8aBfNdaUUEMZEVURRhoQYY0frhko6I2i6iatD7b4BiO2W+J8ycDClo+3QBRmRUPjZA6w5CobbWXckYZO2GxWDg1X3i8IJqx2qioVew9mzYeJ74a4/6EIiUy/ebqGOqHeVTGEuCa0jogZkVixbYBoBbb3yhdLyPa0s8aPGatYmO5XzUTpsXXoDwmXf00yJdgE5Q7vNmxwv94jSp4SAVxB8gR9yvO3iLXqG+3I="
}
3.4 Sale
- Action: Sale
- Pin Code: pinCode2
Request "data.body" structure
| Variable | Type | Required | Description |
|---|---|---|---|
| referenceID | String | Y | Reference ID: Every time a transaction request is initiated, it is crucial to ensure the uniqueness of this ID. Failure to do so will render any subsequent operations on the transaction invalid. |
| customerOrderID | String | N | The order ID of the customer's transaction |
| currency | String | Y | Example: HKD / USD / RMB |
| amount | String | Y | Sale amount |
Example:
/*
Pin Code: pinCode2 (OvSdpyD2FoUNR5rNyte41QqZzR1Y4DVN)
Encrypt the original data:
{
"header": {
"requestID": "5debf769-49d7-4c9b-b6f4-8a9d90e1a874",
"clientDeviceSN": "126498561093",
"timestamp": "2025-11-12T10:11:04+00:00"
},
"body": {
"referenceID": "f8b13b22-16ca-4a87-95a4-df4bebf09ee1",
"customerOrderID": "e246c1cc-02f6-4ac5-b7fb-066cb2b2f5b1",
"currency": "HKD",
"amount": "10.20"
}
}
*/
{
"version": "2.0",
"action": "Sale",
"data": "OE9dcJmLIlqcsAeVyLx1JW7T60f+o32mH+miGIyZRQkLdDuutmQGw/pnYDYPyBOgvCiES4ySbj1UPrUTJWRJaxGTqtP4mI/faqS6iKjPbsfSa2gg8DGc9CnBBHZ6tM902yMJ8n1Jwn0wtfqs3v0NPL3iPb+C5tSqJHpBHwuInndHy/u/Rg/SK+AiY8946eqhYfvyd1mPzl1fMDYErs+6L1ObQkPGiFmWQIuXk0xgdsl4oe7TS4Xci7+33gHpMPLgYMMEztZj6y7A8cUPXXVNHSeqG2+A1EcutsRHJTD5J43W3rsIZH3TMaeiLkLOWY4poHU7RrFoGBUOIh+bP4LtOgpd9+c9wb//cEKAZbLY4AqxyL3NEyySYaZYBUaDhdsd9A9TO2DBrCUm2Wl03LmpuDdiLfzwwrJj9S4Ms6JsCmKrFE7w94n/8/DWD9zTDOKD"
}
Response "data.body" structure
| Variable | Type | Required | Description |
|---|---|---|---|
| status | String | Y | Response status: Success / Failed / Pending |
| errorCode | String | Y | |
| errorMessage | String | Y | |
| customerOrderID | String | N | The order ID of the customer's transaction |
| currency | String | N | Example: HKD / USD / RMB |
| amount | String | N | Sale amount |
| paymentMethod | String | N | Payment method |
| paymentEntryType | String | N | Payment entry types, Example: contactless |
| rrn | String | N | Receiver Reference Number |
| brn | String | N | Bindo Reference Number |
| transactionType | String | N | Transaction type: Sale / PreAuth / Void / Refund |
| transactionTime | Datetime | N | Transaction Time |
| creditCard | Object | N | |
| creditCard.panPrefix6Digits | String | Y | First 6 digits of the credit card number |
| creditCard.panLast4Digits | String | Y | Last 4 digits of the credit card number |
| creditCard.panHash | String | Y | SHA512 |
| creditCard.panToken | String | Y |
Example 1:
When the status is Pending
/*
Pin Code: pinCode2 (OvSdpyD2FoUNR5rNyte41QqZzR1Y4DVN)
Encrypt the original data:
{
"header": {
"responseID": "5debf769-49d7-4c9b-b6f4-8a9d90e1a874",
"serverDeviceSN": "NEXGO-N96-1170270945",
"timestamp": "2025-11-12T10:12:04+00:00"
},
"body": {
"status": "Pending",
"errorCode": "",
"errorMessage": ""
}
}
*/
{
"version": "2.0",
"action": "Sale",
"data": "CcDYsa/fPAjhaaTi5e0SQHJ5AGeWrfhu+D70RJwU9vizGFB38D9j65iEUbBQqs7qbjOQ9AGaBkhNhBWNYUXtdDT5FT9R/2aSLodTgpTPbsWNGhkihyaaVQ+9kN/RjL/frPrvKV1VQsRDEG783RKRLY5Uz0RsSGLXbqkW0drzKxnAvAs13KlcqrfTe0rHNnZ+fKzuW7imxytn1nXKUtGciIXCE+NzXvRSmYbk0WPM+hTCM2WbC+Vul8vD3++vp4v7u4z3d+A7LRcE30W3q6TZKciZ9pnau1A4eVjxAY5EJ93uMWNnny5/yDOE2s1gKHS1"
}
Example 2:
When the status is Failed
/*
Pin Code: pinCode2 (OvSdpyD2FoUNR5rNyte41QqZzR1Y4DVN)
Encrypt the original data:
{
"header": {
"responseID": "5debf769-49d7-4c9b-b6f4-8a9d90e1a874",
"serverDeviceSN": "NEXGO-N96-1170270945",
"timestamp": "2025-11-12T10:12:04+00:00"
},
"body": {
"status": "Failed",
"errorCode": "5xxxx",
"errorMessage": "The equipment is being traded"
}
}
*/
{
"version": "2.0",
"action": "Sale",
"data": "aDmtvHICThbeeWzsusOZbKWWwJOI1PZC5mUHufTvsVr8vO8UyqBw99XfujjCJNq6ibse10OsBLbGc488Ty7qZW/aO8cJi/KQgqVhPU84pQ1wmfbbbZODvlV0PmD9JU6dqjU2BvChhNfFVXKYF6AGXjMh6/NFQlY4G59w0kI1vgNYyRHohoyqHBX9qm4+MUlb0f010DtTPZKMzfRL//KBilJjctok9CWz9CjRim39D969UoS4WLm7x1eXAC/DMPaw2KM/MlUp05BFzcArZlgf1bCtxhvL1TVcwhVQSLHtxYk5Mrc8rIeLMZ0I8Yw5JsbXATMgSmcz+KzaHSwlg7TtB+8rpA1VLelBf0PZmPXwOKU="
}
Example 3:
When the status is Success
/*
Pin Code: pinCode2 (OvSdpyD2FoUNR5rNyte41QqZzR1Y4DVN)
Encrypt the original data:
{
"header": {
"responseID": "5debf769-49d7-4c9b-b6f4-8a9d90e1a874",
"serverDeviceSN": "NEXGO-N96-1170270945",
"timestamp": "2025-11-12T10:12:04+00:00"
},
"body": {
"status": "Success",
"errorCode": "",
"errorMessage": "",
"customerOrderID": "e246c1cc-02f6-4ac5-b7fb-066cb2b2f5b1",
"currency": "HKD",
"amount": "10.20",
"paymentMethod": "visa",
"paymentEntryType": "contactless",
"rrn": "3263492852830699521",
"brn": "3263492852495159296",
"transactionStatus": "Success",
"transactionTime": "2023-06-30T09:08:52+00:00",
"creditCard": {
"panPrefix6Digits": "555555",
"panLast4Digits": "1234",
"panHash": "xxxxxxx",
"panToken": ""
}
}
}
*/
{
"version": "2.0",
"action": "Sale",
"data": "7Z12NF2oZU9zZvQkxAUsWe6fgHZux1IlYJ56DRoK6IpSRdizzq4+Fb5dosmaZZo1rere/21Dxq88IXKKS0yRc3qbaLKwMmRInInvoeSNgOLeSBhAjk8x8P5KJ6m46qzxXOB79SNSxrW+phn5nDiuJ8K/RHYN6yTi0ODRul9fxdZfj0HV/WCDq7IoSYnoDNBuAdm6o3DNcYqB2goGWIJseCpsEojEig58ZgEb5oC9WEUIajhj5FEstEJ2xeUh5DFzUyRryQ1XGO2VGh0eziofOCdYryyjLFD1VkOOP2dtqYJm5Pr1q3GMO5NG67T/E6FLLyP0SWioS+6kLG8qnUiMp2hW4FWK3+BA4xJxHRxXeoh0a/kJKZoe+r+WJow9cafcsLdi0Klv64puK35m6Lgn1Xck28vCaZ+2r/ECPcCnfCjZpTvGB60RhzCBJXY04Sspw40rsmKeS4RJVV4r7SdJBVFX5qtvgdisUTBvgJgIQOHzuTCInTh6CY2HtnzTpzg06KNoYMYN7iwnXE6rIi9c5jCj0T/AWWlX3FpH1x9L4Ie+v2fzNK9D2brlrieGF7v1PbWw1aAvw7MEtdA4k13srOx9ZwQvVc0RQU+Jrom34Gj+QaOMk2XbIZUm6618K8P2GUMrlGOouPEm+WAVFokZCQMNLzi+NXp4u1tsZOwbq3ns0XiVvSAi4hj+c0hmwxbtcJXk+p/ElfnhTW2uMGvgQ7pqyuEO+3dxAWqZ/h2dMiYRx/fYXXVrCRCiBwrdo/kKH2KWouEmIVGrPhoAhhgJMBtIAWav2p0aNnf5RSdkaNhoCMMKHVVdEXBJ5qGQb4df"
}
3.5 Abort
- Action: Abort
- Pin Code: pinCode2
Request "data.body" structure
This request has no body
Example:
/*
Pin Code: pinCode2 (OvSdpyD2FoUNR5rNyte41QqZzR1Y4DVN)
Encrypt the original data:
{
"header": {
"requestID": "c5bf3cbe-a146-4f8c-bb8e-209c1e7b8437",
"clientDeviceSN": "126498561093",
"timestamp": "2025-11-12T10:11:04+00:00"
}
}
*/
{
"version": 2.0,
"action": "Abort",
"data": "uKd8e4L5AULV+jo/XPg4bzMzppIkvDN8NLoB3NAhUwgB+WZ5yMJBifa9gH/nNh/PJe9Kw96L5BFg0tTjK6WqiL4U35DxDbE+99G+PLRK4AJhhfsEDHwMYZGxILtWItM+9+SGD+zoqLDx/72Jz8IcJhVb8fkPHiECllL/Lyp7Y66TRe59OjvCOov4hHxgh5Cwe8m566LK+2gxCgiUj6sdDZaWngBBx4V/4EG2IkZe2lc="
}
Response "data.body" structure
| Variable | Type | Required | Description |
|---|---|---|---|
| status | String | Y | Response status: Success / Failed / Pending |
| errorCode | String | Y | |
| errorMessage | String | Y |
Example:
/*
Pin Code: pinCode2 (OvSdpyD2FoUNR5rNyte41QqZzR1Y4DVN)
Encrypt the original data:
{
"header": {
"responseID": "c5bf3cbe-a146-4f8c-bb8e-209c1e7b8437",
"serverDeviceSN": "NEXGO-N96-1170270945",
"timestamp": "2025-11-12T10:12:04+00:00"
},
"body": {
"status": "Success",
"errorCode": "",
"errorMessage": ""
}
}
*/
{
"version": 2.0,
"action": "Abort",
"data": "Agv/204TIuODU9mRa0g31lG9QKpqY/qD+PSCsN9NjYaOZOW46B9IGL/4qMRIjEo7sMtyMMqwcaMyhoXo6VlvrWwF+nmrdO2HP0QwMYjroYUaTIc8yfX8Edq3UhFG69eZTM1oaX4i8+f/x2PxdsEv+6gIKEqjwHITNG9nFn29ZtNcOcMbJUud7/8SynQXgY9Sz5Q2XgAYrVgxK4WHKWltdktflKGe+Be0I6x6f5s2Gl4YWZFm+BtT3tzIIVUotmUkyxggw7a6PT/BnLWRKzv1Igth1HxMkyLhC6DoiO7+1Tq6/Lq9p2oXJUeCueOV4Z/i"
}
3.6 Void
- Action: Void
- Pin Code: pinCode2
Request "data.body" structure
| Variable | Type | Required | Description |
|---|---|---|---|
| orgReferenceID | String | Y | The reference ID that was carried during the transaction made at that time. |
Example:
/*
Pin Code: pinCode2 (OvSdpyD2FoUNR5rNyte41QqZzR1Y4DVN)
Encrypt the original data:
{
"header": {
"requestID": "8e2c77fb-ff49-4c49-82f5-ae741cb7d3d9",
"clientDeviceSN": "126498561093",
"timestamp": "2025-11-12T10:11:04+00:00"
},
"body": {
"orgReferenceID": "f8b13b22-16ca-4a87-95a4-df4bebf09ee1"
}
}
*/
{
"version": 2.0,
"action": "Void",
"data": "DOdyYB+YN2pT6M7kTImwlruw0Gdn5NHD0Cz3qAgcQaDSr66vQ1X8HtK2eDmeReQIcwjivvwR/+eHFriBT6MssLQLrVD6ZX9Kk427HGFHGuezs9AFd16RLTrl/rODHj3v0efolW8WDXDcz3uzZ3SyRkKCAWvpshL/y5ANLeSk/jAkFRQat/035jn5s80axaJlwEKPh3PNF8QHZY3OFAJqG1UaLMz/DGuteHl3kAKPWNHvUfdVClt+FgfFHvwk+e7njMez638BCMQD3MAxadv7RicdGjT3M+qVeTI+SnDJ4gJJmsPKm2kLz/ATU7xKDN+u"
}
Response "data.body" structure
| Variable | Type | Required | Description |
|---|---|---|---|
| status | String | Y | Response status: Success / Failed / Pending |
| errorCode | String | Y | |
| errorMessage | String | Y |
Example:
/*
Pin Code: pinCode2 (OvSdpyD2FoUNR5rNyte41QqZzR1Y4DVN)
Encrypt the original data:
{
"header": {
"responseID": "8e2c77fb-ff49-4c49-82f5-ae741cb7d3d9",
"serverDeviceSN": "NEXGO-N96-1170270945",
"timestamp": "2025-11-12T10:12:04+00:00"
},
"body": {
"status": "Success",
"errorCode": "",
"errorMessage": ""
}
}
*/
{
"version": 2.0,
"action": "Void",
"data": "wmNcblqksB4rvya2WETsBOJf9sg1/rD1W4xx64cfqHi44SVYqXvmBUNRtaGI4+UirFD67WMmpd3z6vgXZsPgfYnYbJm4calNpBAWqgUxgntjJzDm5YRTTZqCzsW16WDu81XozqSRopHUO6tXSPR+X9pxCAA6TKEM3gf1WibryIgcN+LTUjPnz3NvI8weIQCfZIJWDZiiXbIMPCQTXMTUAklxfbQtXgVmi3Eutt7pmmH+GmUOiJOq1ldSuCNUc2PnRIkVc30DAW9g8nIBmHJo887dzuHP/CeBlUgC4FETdUKM+UDvuNJAJl05lVZSeSTC"
}
3.7 Refund
- Action: Refund
- Pin Code: pinCode2
Request "data.body" structure
| Variable | Type | Required | Description |
|---|---|---|---|
| referenceID | String | Y | Reference ID: Every time a transaction request is initiated, it is crucial to ensure the uniqueness of this ID. Failure to do so will render any subsequent operations on the transaction invalid. |
| orgReferenceID | String | Y | The reference ID that was carried during the transaction made at that time. |
| currency | String | Y | Example: HKD / USD / RMB |
| amount | String | Y | preAuth amount |
Example:
/*
Pin Code: pinCode2 (OvSdpyD2FoUNR5rNyte41QqZzR1Y4DVN)
Encrypt the original data:
{
"header": {
"requestID": "0f7d24de-76fc-46f8-861b-2d0d31a2dd9e",
"clientDeviceSN": "126498561093",
"timestamp": "2025-11-12T10:11:04+00:00"
},
"body": {
"referenceID": "97cc8c76-65e0-4bd2-9e16-9b9e09f65c42",
"orgReferenceID": "4b02c099-4dc9-4b30-bd2c-27bf3223df6b",
"currency": "HKD",
"amount": "10.20"
}
}
*/
{
"version": 2.0,
"action": "Refund",
"data": "9s+NYd52kApZBoNgI2PogmN02RP/CFmOM4lrFnAL0tTDiz/g8dlKhvNUby2uo8Yuui6OqRBHpPTkIR97ov9q5E93EWYsDSBqpEZuIx/FF5q05GR2BkEnWiYre1+ZhROnihycHQv8HXIHuihB+d+n4bGMtjxFyV4t9hbH43HBqtS+d6yXMBNObWA+WwIZKPwAUq+rnlvSfuIoeIUwoTy6GqhEK8R9I1El05Hc1Kg0KJBdGHlXLBYU+xkQvGPn3C1KRUG8lmRIzTXaIDbNw+4GAhq14IeyhXfzhS6JWysUza8XTOaXUiKYc3Ka4gb3jTt/ZynCTmjE0A/Za037nW4AFAZy478Bq/cqkgZP8qvVVd7qYxkqpe+dBM9eU5BhYSqkIGvpOiDiWTBy8TXqRfexRbkDAkRFm7QD/ng/P1dJO2Y="
}
Response "data.body" structure
| Variable | Type | Required | Description |
|---|---|---|---|
| status | String | Y | Response status: Success / Failed / Pending |
| errorCode | String | Y | |
| errorMessage | String | Y |
Example:
/*
Pin Code: pinCode2 (OvSdpyD2FoUNR5rNyte41QqZzR1Y4DVN)
Encrypt the original data:
{
"header": {
"responseID": "0f7d24de-76fc-46f8-861b-2d0d31a2dd9e",
"serverDeviceSN": "NEXGO-N96-1170270945",
"timestamp": "2025-11-12T10:12:04+00:00"
},
"body": {
"status": "Success",
"errorCode": "",
"errorMessage": ""
}
}
*/
{
"version": 2.0,
"action": "Refund",
"data": "NfjFtfEdlWXJ/sXqaApef8FQcjoOr8mZynJlBfOw5OdOgtvUUdB7GTbGJvNt6Ye94DsavJQhz5+drVUaSwZVTB8BVmBN/CsE2tfcPz3Bu9F59jzIPU9+AKMjp1/1aQ6/btm6bEcauWSZshQE+G3oVOgM0EgI/ZFGGN5n7i5OzW6tCWvLl9SvoaIXbsolp4AT9tzO51nKl8Plr0y7jTOatls6D4sSxPnLguuQJrqA9Q/wUMMYakwX+7MaedMIP0iitImsvoLOJRULTVLRhCbv0FebFZ/X1sDeSHe23yuLlnbdlhpSXjToRsWgCUtOLALc"
}
3.8 Query Transaction Status
- Action: TransactionStatus
- Pin Code: pinCode2
Request "data.body" structure
| Variable | Type | Required | Description |
|---|---|---|---|
| targetReferenceID | String | Y | The reference ID that was carried during the transaction made at that time. |
Example:
/*
Pin Code: pinCode2 (OvSdpyD2FoUNR5rNyte41QqZzR1Y4DVN)
Encrypt the original data:
{
"header": {
"action": "TransactionStatus",
"requestID": "18fd2b62-6f65-40f2-8b94-88ef32f07a3f",
"clientDeviceSN": "126498561093",
"timestamp": "2025-11-12T10:11:04+00:00"
},
"body": {
"targetReferenceID": "f8b13b22-16ca-4a87-95a4-df4bebf09ee1"
}
}
*/
{
"version": 2.0,
"action": "TransactionStatus",
"data": "3U0hWqglZgydHjzniajpGTJcl2rjH8b1jINmfnDKq+c6ogfrhxVpjtY9AybhgmP9s4mMo2BvPLKMumyzFfZ8n+nInKQTMPUYvIB+TJqV9wdxhqORsfIH8o9hLJd9ot31opwxVdIqlGdU3S3qNzEeZQ4KmqcbyneolQv6niBkiHNf3dnyZHXv7gr6yY56P5g3gORRK82a2JZBkZGwPJj9Me1aazM7QImj1AiSzaBu9Q+fqSTcYvtF7JGe0rvWp6BGXoBdlN2MEJPC+UtK0ZLdolwtaX5HV6wLeXm+dRh/6BCC9cXugE0XY1WCFSqcqxs2C9yagn5fbMwd7ZhVJ2qewI9Mc2TCkBAYQcZX+RRDllE="
}
Response "data.body" structure
| Variable | Type | Required | Description |
|---|---|---|---|
| status | String | Y | Response status: Success / Failed / Pending |
| errorCode | String | Y | |
| errorMessage | String | Y | |
| ... | Any | Y | It is consistent with the returned information of the current query transaction type |
Example:
/*
Pin Code: pinCode2 (OvSdpyD2FoUNR5rNyte41QqZzR1Y4DVN)
Encrypt the original data:
{
"header": {
"responseID": "18fd2b62-6f65-40f2-8b94-88ef32f07a3f",
"serverDeviceSN": "NEXGO-N96-1170270945",
"timestamp": "2025-11-12T10:12:04+00:00"
},
"body": {
"status": "Success",
"errorCode": "",
"errorMessage": "",
"customerOrderID": "e246c1cc-02f6-4ac5-b7fb-066cb2b2f5b1",
"currency": "HKD",
"amount": "10.20",
"paymentMethod": "visa",
"paymentEntryType": "contactless",
"rrn": "3263492852830699521",
"brn": "3263492852495159296",
"transactionStatus": "Success",
"transactionTime": "2023-06-30T09:08:52+00:00",
"creditCard": {
"panPrefix6Digits": "555555",
"panLast4Digits": "1234",
"panHash": "xxxxxxx",
"panToken": ""
}
}
}
*/
{
"version": 2.0,
"action": "gBzyZALiwFJKCafXs52mAn0yishykDmpTHSSEy7i4HYVulo3pT816TrdZjw9P6ha7eMIeKM3iror6BXNtKFI38vzSLykjLOHCKuwUnAxdsph8exTzklpsgLcplJ2Pt4dEJbnEPRvsX+e1e20tX+pJcoNkR3psyvDPAfHoMsZgwERdmgdl7HbcwQd1UPItf7EICVGUAzksBCkwvDYq8E7FRPNV7EbJc4uIGiWSCY2Q0bcs23k16/K5O9+uEEgo3j6J/eooEzuhhkZMx+rETRAud9KLFr0OXpfwACwR4T18jBWF9LU6YxGM2bqiBKLsE8Gctj/XgXbZuQK7zgunHXE+xgqZgiHQlKlRsecJlK3Uf59O094RsEyI8yyJQd5kT/X8fIoMpLCzQqs08IH91lVvQws1ZzipDy/DyxYIBpQzeVQYQ/OCkIDxy4s7ew4hWlKCTSDW32sDUTwA6aej0U+jZk71eUSWhQlVX63br9YRhZ9n+mSI8Fkix3FHerzVBdsbDSlE88fP7vj9N96YIyOILRVjCcJRIZ1ixlxtRwpTTw/In42j6YLffvkV0/aGQjQYeVEnymdGhC/HhlHhSlWmGjbfiJ3UXxsxSyeANeccAlV76+7t6kME55uPC2AU16VYIwfWsj5T7cBSsxaEqmVlQiOgiLNRbR3wp1Q4azzCe1owFD5xt6SWds5zM17cnLlxgqonOHboKUIFDq8lUx/OXFDk8aBcqYJ4wYUau/3iH1ItZi2hDzOsMf5AliRMytaW+TUS/szyGcBGIIEWVEUPeuDoTGAVDtg4QV5TYpKcTmZneiYWXA0OziglknyLXZ2"
}