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": {
"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 |
| businessID | String | N | The unique ID of the current merchant's business |
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": "pePKeo2urjKmSoah7AWb8d9JDqjqtJG781Yh8NxICIbJ72y4kgxuau+yX6bXF/M6gz9oyuIJC1Q/shNrwKP8Ev8qzoZ587LtPOaQrENU2W/p6ue7PRoY9C3pEo4M2bZeQlELOQj8ohKqmEdrlcntpJem3Q+yFeXTPxAwvpKfKTgYbpdHQSC//5WnoxNGnAHLaqRTaEDRr0wWYAcdb7mY0/h7yNkkSNffPjmvlMmDBTmWP/Dq+c7k8otszx5m0YBTyOFxOJipLQytMwoZIFzG/3m3H7jMH68VvWemhp+DPh5TFa+cgoZpASaoz/oLylv8"
}
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 | Y | The timestamp of the current request |
| businessID | String | N | The unique ID of the current merchant's business |
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"
}
}
*/
{
"version": 2.0,
"action": "Pair",
"data": "id4/0hIX8IG0GJuuH4OeLqHy50LdXMoG96A4jPNhnpl37h+Y6z/tvkisdkJh2kfGBjLQtOJ/TptfeHkNT31mL+kEvzArOg0VxTTd2AksF1qShOQDvK6Xvqhk0MsT6Vx+McSA5HJGp+cvNODc64TmZ/Z9qH7Sg/rcrj2uPq28a1YfB19FNL5w0CmvIj8VfXvaNhaBSguFodQWqP5XazvsHtI/E6Nyk4qsNIHNFlb10IKWws2Kgzg6voFYuZMBqKcu18iHTZcMc+YV/4bShFA3QuTR9562U91bCgebTq9cdgV/Or0VDZbfcnCY4EGM1z7G5oE6IBsdPmDBAS45X/279gDd1lgoqSX8FOsRSfwCldjMUUfKFvsH0m7OKGopfjRW"
}
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": "Hlf2xR22p3Mkc2Hvj0XrvlyZmb5vNk6MxzMDpfE86Y1w5jxK1HYqeWgMGzjlEFr3QZw3N7Czpm05NlBLfAyhrdCW/V5Wcf2QsfAvOQV4cNP6VyKR0XwQHXqydqJwzrDftJ+6uamLYQlJn1D4UbQtFz1oR9QCJlM/KJHLcup1IrqZ6U7WN+N/Qi0bxWYcVcEn4KVegSTDFUIUbabXlAJoYNQwP9AocaBVegm2orKbFgxUGlFJRxFrjm5I2JiAKSfi3cFOezyQ/2BB2jvfuGTLoEsujuEwLkfRzmn7cxRXfDhNncoVeOVqu+HcGYcmomrh"
}
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": "hvii5rlCFHUh1o91yfaNKDbnXIjpLOzF+rQ+D0/GVx7HjBU4oM/YRk41tMnkpFeagXXFNKgBrMNQzQC7ohU9R6OSbGKQMzQvgarrEY3cSHmGv1FuVL290jcEzD5bW99CJSi0bKeo/oTh/aPLadXp2gZNRWEkuP8sEZCM7w7pv0pQvwcnfJKBnD6tgkDgMIUPs1QaNIGPom0s12m9k4R/iQmB+8A6UWkscSd48g85ZJ77WWAeuQtmgRoW8/VNss5aRjIf6CBH0OYE3rYCACYl8fIrBMLsRBl24NmIhiTSt97bKOXFXMugoc+/jTWAz+YgVXadkUTUhG1QBxhJanS0EEMu2x5Q/Q2IMaX8kWmU4mZOFA1yrfXSNSzYdtS63P0SPaSw0uB0uObF5K7qEeYkhI8HDIu0dLJklXafbpWHBhAUFvUUNnbtW/WXvQd8xwzs"
}
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": "HyVyYTEFadRFQyO3ErhsscZbuwH2nmzt7WcqaX7zwOU2dYZ2kK1nkwKTxE7FDVR1Lcra0yfcOIZdxO/qdAv2NRDwUHkMiBvkIxZMVlaM+3CUdhpqG0Xtub28gt9Z5KI6HpeGdbSGHm1qDImHh+DEVEe55mUew4/czadIQ3kvpVv2vk1U4h2NVWjkw6C4EZMtGKnWyMZmraUxj/kE8LRvckRiYy9MF+v1Dq2goIwSHwa/skZjHJ8d5ZbGrssuWMJgXJCflvTh3TFqntcIrASYobNVWcPsUByQS9vYuZdpDU/PuQ7NyIvXSGp2vmJyUpfq"
}
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": "o8DusOr0hm/aEr3flPjUYZ2dIBztnOz/KbO+VL/9deQvsZvREYwn3Rks8N3B4+XuoKraTp6mlh2DsMTU7z/scvgy6jwnWFnVD8O1aOPE6yZn+nvlqohCwkxgqBRS0/fSqPE0oWu8d8BNBQAGgHzxmpPwogcNEoIUE9DmKnJ9ZXXskoG3SbPdUoBgtKsJiX3cDvS3jOK1FPPuOoSOt9pSf1Kt71/wQE5bLozJ7W3UhZuzQ9MqGa+PGKBPjGqUn9GXx4Pvp2sJBy/MasMIdIcPf/XTJNCXeXEAaa0A50cEbY+/1fYeS9GMD9RATln8mnjy/k+UBqCE9T6n41xCXcCyw6hKtcvfgmH28JXY4tSxpIKMGFityhMZvdFyccdR+RW3YqDYHm2g7vfyThIoAl/3pV2KfhzXc/Po+pg0Y3I6yoc="
}
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": "LzskNnsYgifR+ZjuXVjPDfT2Nw3YhbA4OAmT2yBBFgR39Q5TjSrnaW44czmmBcVlr2GL327sHk62o5FMXsqyxUNJC0uq1hEgp1gTzxzLYNnkwqbb7NeSFfdscqReOPiOMbY7rWN6sjfoVpVbTKSEVYmhZAVx10TrzSPE7w63pHNZl6utlzkcZNVwZSQO6qqkXtAGbKtryxZ1J0VA8mIt80aDKqEpECYICV1twM6gHgw="
}
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": "ZhnFiGfiT5Oir46CtHaQbVpegL5pRkNELxhOdN+XbqzIiaUmoimyxD3VYspmdNhyy2ZoOsrDZGvNhOxp/5/Cf21XBb2j8P8Gl/eMV7yHafjvqhoYGQY+m2PoYYAwBsWH0XDmbti+rfg7VQFFvHBoOqnfYmktcSJzHjm5hX4qyUMQdNWeTF4e+pkZ3jvTTLvdzMZlrMjwXOMxE1ikrbR6sgCV2cFx2Lrdvlh8gJdE6SM35CZna88X/s/tMPe2w32Lh0ou2eHKlARBuCG69KxEp7RpBQtw9+PDrzX7u4bL13V6b6LZJnejuwIsbxIz0yAPr6eApB41Vr6pELCXOyh1zRoxlF8RN5c8AhYE5NrSLCcvMOwZxvGjihrePAjO7vJFivowjoIwYMad1H31uLCIGrjOoD2JEycnpDWuGyRZcgU="
}
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. |
| paymentMethod | enum | N | Payment methods: -credit_card, -fps, -octopus. -consumer_presented_qr_code, -all, If the payment method is transmitted, it will directly redirect to the target page |
| restrictedPaymentMethods | Array | N | Payment Methods |
| 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",
"currency": "HKD",
"amount": "10.20"
}
}
*/
{
"version": "2.0",
"action": "Sale",
"data": "x0YiJ9xhtmSxU02TlNC1TVfFY1/QKuQYniemnpEakcEiVmEDuMWQgFmnjAQrnl16omCQkOWW0sQuC9RjZH67gCfWBpQAQlO10RWw/R5XBmGs1jeJlND0IEKz6BLhbCflbME+GkZLyQ45D2vZdtI9KTSfy/CRmHdzngI31wU2002WUeAwaZt/R9teYmkXqrqtQBVn8j0gGpm4lS20hDqqIOB4olb4QgjFINopRh2eKGSTKsCnFBBWcvYiXYYHw/tDeQTwMRrIKfYYKU9ASmYcy+vG84G4renwsieFLWbp9hSfBpozjUOxNl7C2ziOdOYnOlzABW9a1MCLFx6ZJFcuGWb1rsrvOB1i4PAEtIH/ar0="
}
Response "data.body" structure
| Variable | Type | Required | Description |
|---|---|---|---|
| status | String | Y | Response status: Success / Failed / Pending |
| errorCode | String | Y | |
| errorMessage | String | Y | |
| currency | String | N | Example: HKD / USD / RMB |
| amount | String | N | Sale amount |
| paymentMethod | String | N | Payment method |
| paymentEntryType | String | N | Payment entries, 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.consumerIdentify | String | Y | Consumer identify, e.g. Card Number, Open ID, etc. |
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": "TOGegN7Gul4p+OeymxhD12sB2u6G50/SbhM+pEbY45IpW9gdVPWx7fxQSxvRK44lClO5+9y7vkaCfDksl4Ngq0fm1Uq/1v4rUhwKAt1VJS8f3lHgSWT0UaKu0qNY/7ynJmBRpjfSPaKgRH8lWTHMhbf364GE9N0Rd8CDsbPUEjSR1LTtXuXoKSOqQeywrADg+D+AAQpzz+iwq005HIzUic/L9PD/JiXRcER0Qv3ngvvmpUEB/V3ycvx1sFN/1Ja/3ne60LqYOBVRa1aEPyU0WbNGO4+Yu7GYc+XiOXgEyiBDtcPQcFOeDi+PanWLtp1k"
}
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": "s9j7u8roWhx/55iOwMIY6ePbKradiQGo/3gdzDcCJMEEIiGZVQ0g9C7fGR56tSqw2p4jdU0iIoZ9zOuZG0oUmsyqqUXvB40bMYLSpfrlBhqvGYAoSHuXBtqDKr/ithRvIj90y0apTv6ud0W7eAfb8YECGuniBaOFdooXI5CbdPzaDhJ84iaXF07KmBmjBuWPlM4wMMUWVghtFiIOEC08HFkEuPdRQamABD+yYUao/Nv1NuHG0+geN+CVHPZkEJQgQZDlJA0NdVkHI9qlwtr8V4E1Onw6/dJeXCXqSj351c0HzxMTWWSuGNSbxF48YrDIejeiV4VxTs5GokpKgSEHOaPvPhsAB+lRenv1JAtqZcM="
}
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": "",
"currency": "HKD",
"amount": "10.20",
"paymentMethod": "visa",
"paymentEntryType": "contactless",
"rrn": "3263492852830699521",
"brn": "3263492852495159296",
"transactionType": "Success",
"transactionTime": "2023-06-30T09:08:52+00:00",
"creditCard": {
"panPrefix6Digits": "555555",
"panLast4Digits": "1234",
"consumerIdentify": "xxxxxxx"
}
}
}
*/
{
"version": "2.0",
"action": "Sale",
"data": "zqZex4ZAGwgB5NmBD3FH4FGZBqld/s1dqLltYfIf4TPljkvuy9BmK5Vq8Cs4dY/gFZO9dp8L3xEAT6TcnT1o8aGx5ffy8PWkrB4uXo1cQpoc8h534Ph8QTmqQ2LN9QUf0nOdAK2u164KUETHPFgMXY8J4Y2Z8S9fHJqp40XIHbUCo98bXJg/f+f4pslampeLCeAl56iKBTiKNy+Z/KHGj6SHzFfysJk9iCewWn9JZBh+Pts5e3o0fhw3VPVDVSKEgqceAyrIzUD834KpuZocO9Xk1XsxscC7FBB06GBJg8wUCqb0NqLGfMmmUdC5XAT+DOQ6q80U+jKjkY+m5rxTJEeQshzbP3Ulo/k1rCyk80czbH45GlhYPLNxiWPaJgVXm05GL6ClvjACx8ASBKbG2cyn3nmoI53s2/EsT3nLZ1rOf4SC3Di1pfWOZB2hEqCkaDmZTVFMEyYdGsMlO0nl2w3KEq6zf9LGLXrqDhORPq73u1ESckj4zNjHsLoTP8/ZyWc/1J2OLJRcxk4UfZ3C7vzkD0zIc8phOCZFbu4vRQZ+W4U0ZK9FZeDOby24z4NSHRut5zMLhERIWwW+A5ozXHkNp0JU+yjln0JkAemzy8BmPxWl5t8Y0Zih5rbcZbJj7j+xri9dz10KfdAvU7mAZVllrheCpt0eflbIHCSyMUUl4Km1nnNaeC2/z43tbvGhthEhqr64Yj5U4GSJWD84WVrwS/dzhC29d3TOsxlfC3M="
}
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": "AG1mb0IbA6EzpyvE6g6ySqemX3XiXLVm8wlGbPkOfsRNuxSCy1fz3/VlDqRd3PxDvy8FHCv6pvB0nhxysM+V85NNbQTC15+GgrnLeupS9IMSYYPYAmhyXtqae1DbXcqCj9F614QJIB0bWzRpqWmOgap/uzTIwFHf/qGMKCCCADouMiDxnirp1EunsRBeECyBLdlBoqPaWoNhhMISyZu5+9eul5E+eZMmKwNUDL4KJw0="
}
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": "MPf0p+jMU+smn2s/S1so9c4x3YLmJeJ3TzXbtiniGAVFhjYJrCCOdC0wc32c738qd1D5F8WlaWXqz+uNtQzgkLgmx3wz/rDGBNRh11tNzY4AfZEra3JfWEWEsNDFpDco9FMjF4mO/AhD/KFYe5+1suMCtJdLYE5ka9XnJr3xoYd8YrHdkY3Bpq8/tveW8FHIwMOj67hyOvtjdtnemv/uD52QTcSZMVCF6vQFbK9VJb74tE2kbGWlWmYxtWwPRwrv5gTh6YSdMESoFxITjE29SkVXv+oNa85B/NqOMSFEc6AsgIKV0xCpebIPOM6Owz6G"
}
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": "p3Lowjj5f+OD2W2K2XeSrbpk31s+5jBe+5fG4XdqMU2JE+8jDN51akS1+sq5DvDL9vMKUSlHEO5868yNb7hbA/CHCB5xein9imTEaelM0BDpoTnew1mRkELIcWZYECyghXlev44oIhBEwMKHqQdUwfpKpPL3dr7CmLmBP7+Uiau1M3gai4V9Y3ClQqgCROqOegLZ9TKHHBtA9k5r7ArDlKShqjYmtXA0ASvkxc01tHFQbcrLAeW8yOA8NlQzO+oQrpafWE+5CSEqWP7OZlc5EJi69/X2Zw9x4H3p/fl8SFjEf3Og7RO15zIZTd708cju"
}
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": "hknIQAGtuz/m0wpmRK7ya22HBiS9jI84cu2KHlCHPbVgQj0urBKWKEXopqnLESWTvyezR5XOwMTFSzTc0vb9/qySDYVXJqyHsynPrI3Ad6NswcBbkwKWVjT+vmQWX6IWqGqQyMDGjxKX8AgZf+3f0xyJTgOEvUPRC2+6sNaMfNR7szcfMxhZ5ji6GrL8xT6j/b/8CxYSsA/9f5rC/bQehxSHf/iUS3cLXABMfZn4wUqmfuCCfSXtgdlNSa3IT6K860sOJfnn6LLKqdT937vNqjeF/mOgrZnjdwxFGokgZNKdPdHbmc1TDwAw/QHxk1+E"
}
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 | Refund 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": "y7D+Argj9zj7/+Z2shQY35DvhruwzC3IMLyM01HL6dlE6CFTPOgJuxv9aHb0VaoMu4b9jRaTjaCHu5BPjUYS+XbzYlDngkuiAloWpAgB2Z6Z+VzDqPK/MV0rKTwdCrcYcIpgnDCzI7G8w5Q/0ey8C7scx/fvsHlYmCG1DOjYA88jmJam10vm2Y4fVTsz4jR4Nh9a1qjsf/JgsaPyqf/F3nnmH2Fxov2fc8xTf7Zvexk0ShotgG7hdQ4Ifj31Tyqr36pM5bIQa70eKuq/SuFp4O2IGpfugb/9ztMRp7jjZ3qm2cVpxJG5IcpMfdDZGWFJFb9jz0Pu5iN0BKDA+CibBnb4wUCZK1A7nNA94fbpPJX8ehuTOoTiKbR92Gihq/KXtpRDwGzKvatOy2xdND1KE9fH83d6pmaAoiYrUaQiH9Y="
}
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": "L13PVs3Bj9CCqFHboha1OLUdQZ8/BNP9ZX0pCtp7bl5Uvy+8xhUoohBxyZR1pwMMf61Uwpdm+ivQ11dcVK8X3NjB+AWkF6R4dIW+n+jooZ5RFf1dpDB3EJS4BRhACf9lq9htJPnIqjvWchBLSq/AGEClCF6C1rTNn5hF/p2pxFPqIAJHpxEMV/VjIsCoZGZ3vh02MD+EvIl7DFYmvcHprsnyw4kJ6oy4QfUqmbttBkeDocYl9w2ofuXh119aUYoqflazUUWPvclyoh/tanwvMk2zoD6zBFhQBdXIrG9ONqEpIWTMF87Uw/SMnQ0PCmwx"
}
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": {
"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": "PFR7QydsxUlVYXFh9g7n1DKLUx5DtPLNOnSCCYIvZPAg9aZHx1rt6rNmW1BheI2MQO+Sq6G3LC4DlLxSQxTwOmN7mJREzckYVcnze9GhbENDP4tMmbLErjlU5lgctk9rkRYs/hNTZThfEd340c5JAM5JSPGSGyNZVs9Yf4FQa6hGxUcfW7RXu9dVntrHfPwuwg1A2vTSeh6QdQEgNJO+9jHygoD45RxiiMbPVFYkCMq5gve8Pcwwu950ADhTwK4kzKm2yyuZGjl7okuhAtnY3ADJ6nZzED94Q8fKRHs1VN7vnHyxIDVd9B0hNqLOacn3"
}
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": "",
"currency": "HKD",
"amount": "10.20",
"paymentMethod": "visa",
"paymentEntryType": "contactless",
"rrn": "3263492852830699521",
"brn": "3263492852495159296",
"transactionType": "Success",
"transactionTime": "2023-06-30T09:08:52+00:00",
"creditCard": {
"panPrefix6Digits": "555555",
"panLast4Digits": "1234",
"consumerIdentify": "xxxxxxx"
}
}
}
*/
{
"version": 2.0,
"action": "VOSss8buM+a+5PbdwyLeAsAckCpeJX494TT5i3KZpApAft7cfLFPkaQJesUK+vnRQQKfmRCKfg2Bm3iKRe1Tj/E1OvkuS7qpP18ZVXu1Z4kouiyvJLayyUqw5Qodg8IB2xK9giFmrvm3Ss9EUbBUnyjSDcVw1jUxHqy4q59fxJ/LRKKNya+19YuqcXlKlpiHrAdsdjODiHTXVv0cDR3QSvGGmm3ZQPpK2DBnPuuUym39Gb5B9sgEn8Sv49GyYdx9l/RBBL6XSNDMKBMMQZ7xcj4LTDP2ksZxF1alhuugSCXWnxBiNpQb3HIa+lyQtT/TXt5miFPDG+EvYHKVrAqeEbLbKY3QUqPFr3AlX7QW06xsPdOrsFrafwMgasZaTLd4TFtAYDpuTxFrfpJbN/LLGMSjFq/uyn/NLDXGwPerKGSdjurdmQv6eJ5lBKcpLGePXmIKBkynSeC63iGUK3MI1gv0wqwNezcN/BtQM5t2yyv0FbxL3I+133x+VKV0uP1xYf5rZ8gpeKaRXUYEoBtb+LM7VhlDp5ZWumxzfHr8mVt4Z/ikvTmaEEAdpTdvllNDIw3YaBbRIjK1/y2qhZhx8ohULIg5aom0NjgVS9YWvp0G9sayeqYd+coQa9knBT7KHgOv/hJNQWZs6CZ1YIhjbpC9qhkbaCr8PNbRJHnJbrFMVO9QrSGXnD7Tjr5pXMVYeg+R3C/zicbKKcs7ZyWzp00x2/FdtvtzbPmYcXIRyG0="
}
3.9 Sale Directly
- Action: SaleDirectly
- Pin Code: pinCode2
- CMD: 0101
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. |
| qrCode | String | Y | The QR code string |
| currency | String | Y | Example: HKD / USD / RMB |
| amount | String | Y | Sale amount |
| timeout | Number | N | The timeout in seconds. The default is 0, indicating that it will never time out |
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",
"qrCode": "22349671249672146346",
"currency": "HKD",
"amount": "10.20",
"timeout": 120
}
}
*/
{
"version": "2.0",
"action": "SaleDirectly",
"data": "z/IjDI8eUlFKCTrCbtt4U1EbW77HfYZYglG6HApsR7xMJnhLVJF8BlyxQMjhR8ALMMLCu+HAGAL/WMrHo0BZPdwdCHyLQnqeEJX4F3wshkiBkw44XOrjk9H2UxYGwammnJF1WX5qFpHBqEsvc8TsDQ1tcpL/Xsb7Z8NyUYG4M/kIQDRXLwXHs1tohf6Xop4jhXOP+3VWJIkovUJo7pZi+voj0AwjpRofjUk9I/J5pgazU7UCX2Q8ag7j+rhj7XoNDj8GvhkL0OPFcWeEbjeunZBt0xAeOThYDV4B3v/+ycUIQ9u3vFbwnIcbzfX89hRkEmgtY1clCUv2xBFlbh1v25tx80/2tsycHYziWjct3TzW84SM80POcCso68rcQo8xqJokgt/kn/bZb2t8pxs4gviN0LK+324VDT5AzHJxhF4="
}
Response "data.body" structure
| Variable | Type | Required | Description |
|---|---|---|---|
| status | String | Y | Response status: Success / Failed |
| errorCode | String | N | |
| errorMessage | String | N | |
| currency | String | Y | Example: HKD / USD / RMB |
| amount | String | Y | Sale amount |
| consumer_identify_hash | String | Y | |
| acquirer_type | String | Y | Acquirer type |