This commit is contained in:
2025-09-14 01:58:11 +08:00
parent d40b3bbd62
commit 165beeeaa7
9 changed files with 192 additions and 7 deletions

View File

@@ -0,0 +1,3 @@
APP_ID=68c47321131b9b0001166b7d
APP_SECRET=a9f02a4bef2e29b2b6f866d9d2aff2e6
CODE=ecd82af4267247b1a1a59b6df7fdd320

View File

@@ -1,5 +0,0 @@
{
"encryptedData": "NpiIHuKkhY2S8pCG9WShtDcCLmBhnKkU+m0BLUHJKWkUFrkPay6j3Lc7wkzKat8U/gWsP2WfcMHS6nz48VvjyyKAeXrAxQKEFIxLKslRqntEJnvu5HmANssOypHHk7Y7ovij1QvY+Od/pTBKL73i2AzLoNtXubMTWSPcqG0A/Ov1uy4U7zRZpUIa5otQ+o5dqHc4+rj5EnT2MQ73+QGcFA==",
"errMsg": "getPhoneNumber:ok",
"iv": "aUFhbEhJbWFydjVNQlE4cg=="
}

View File

@@ -0,0 +1,6 @@
export const demoData = {
encryptedData:
'HdZv6GsUqB68aoLf+SqSaGSqffOTMHHW89DTjh7suCIz9gsdK0wUTpL8+60+zqKcL3b1jNxrRdmZxXGHO67eH1HG5WSoJU7Jlu1WjUWzK9ZhBuPJNMP7Z0aHx9aPOyk1grlZNHOA51/i+AlKywm9QY3RLRTtPQpoSTcjJud1iWhF+aYBrI8pS/rw/AYQuRvOuODjQXinyAC6pJPqZzfs5w==',
errMsg: 'getPhoneNumber:ok',
iv: 'N2dPdU9wT0k3bnNKZlhsZw==',
};

View File

@@ -10,5 +10,10 @@
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
"license": "MIT",
"packageManager": "pnpm@10.14.0",
"type": "module"
"type": "module",
"devDependencies": {
"@types/node": "^24.3.3",
"crypto-js": "^4.2.0",
"dotenv": "^17.2.2"
}
}

View File

@@ -0,0 +1,62 @@
import CryptoJS from 'crypto-js';
// 解密数据
// sessionKey: 微信/小红书登录后获取的 session_key
// encryptedData: 小程序通过 wx.getPhoneNumber 获取的 encryptedData
// iv: 小程序通过 wx.getPhoneNumber 获取的 iv
// @links
// https://miniapp.xiaohongshu.com/doc/DC591932 // 小红书小程序解密数据
// https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/signature.html // 微信小程序解密数据
export class BizDataCrypt {
private appId: string;
private sessionKey: string;
constructor(appId: string, sessionKey: string) {
this.appId = appId;
this.sessionKey = sessionKey;
}
decryptData(encryptedData: string, iv: string) {
// base64 decode
const sessionKeyWA = CryptoJS.enc.Base64.parse(this.sessionKey);
const encryptedDataWA = CryptoJS.enc.Base64.parse(encryptedData);
const ivWA = CryptoJS.enc.Base64.parse(iv);
// 确保 AES-128 使用 16 字节 key微信/小红书 session_key base64 解码应为 16 字节)
let keyWA = sessionKeyWA;
if (sessionKeyWA.sigBytes !== 16) {
if (sessionKeyWA.sigBytes < 16) {
throw new Error('Invalid session_key length (<16 bytes)');
}
// 如果大于 16 字节,截取前 16 字节
keyWA = CryptoJS.lib.WordArray.create(sessionKeyWA.words.slice(0, 4), 16);
}
try {
// 解密
const cipherParams = CryptoJS.lib.CipherParams.create({
ciphertext: encryptedDataWA,
});
const decipher = CryptoJS.AES.decrypt(cipherParams, keyWA, {
iv: ivWA,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
});
const decoded = decipher.toString(CryptoJS.enc.Utf8);
if (!decoded) throw new Error('Decryption produced empty result');
const decodedData = JSON.parse(decoded);
const waterAppId = decodedData?.watermark?.appid || decodedData?.watermark?.appId;
if (!decodedData || !decodedData.watermark || waterAppId !== this.appId) {
throw new Error('Invalid appId in decrypted data');
}
return decodedData;
} catch (err: any) {
throw new Error('Decrypt failed' + (err?.message ? ': ' + err.message : ''));
}
}
}

View File

@@ -0,0 +1,45 @@
import dotenv from 'dotenv';
dotenv.config();
const testBASEURL = 'miniapp-sandbox.xiaohongshu.com';
const prodBASEURL = 'miniapp.xiaohongshu.com';
const baseURL = 'https://miniapp.xiaohongshu.com/api/rmp/session?app_id=${app_id}&access_token=${access_token}&code=${code}'.replace(prodBASEURL, testBASEURL);
const accessTokenURL = 'https://miniapp.xiaohongshu.com/api/rmp/token'.replace(prodBASEURL, testBASEURL);
const getAccessToken = async (appId: string, appSecret: string) => {
const response = await fetch(accessTokenURL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
appid: appId,
secret: appSecret,
}),
});
console.log('Access token response status:', appId, appSecret);
const data = await response.json();
console.log('Access token response data:', data);
if (data.err_no !== 0) {
throw new Error(`Error getting access token: ${data.err_msg}`);
}
console.log('Access token data:', data);
return data.access_token;
};
export const getSessionKey = async (appId: string, accessToken: string, code: string) => {
const url = baseURL.replace('${app_id}', appId).replace('${access_token}', accessToken).replace('${code}', code);
const response = await fetch(url);
const data = await response.json();
console.log('Session key data:', data);
if (data.err_no !== 0) {
throw new Error(`Error getting session key: ${data.err_msg}`);
}
return data.session_key;
};
const appId = process.env.APP_ID || 'your_app_id';
const appSecret = process.env.APP_SECRET || 'your_app_secret';
const authorization_code = process.env.CODE || 'your_code';
// const accessToken = await getAccessToken(appId, appSecret);
const accessToken = 'c525e1c9125c429e83b699acd00ac858';
getSessionKey(appId, accessToken, authorization_code);

View File

@@ -0,0 +1,19 @@
import { demoData } from './../demo';
import { BizDataCrypt } from './decrypt';
import dotenv from 'dotenv';
dotenv.config();
const decryptPhoneNumber = (e: any, config?: { appId: string; sessionKey: string }) => {
const { encryptedData, iv } = e.detail;
console.log('Config:', config);
const appId = config?.appId || 'your_app_id';
const sessionKey = config?.sessionKey || 'your_session_key';
const wxBizDataCrypt = new BizDataCrypt(appId, sessionKey);
const decryptedData = wxBizDataCrypt.decryptData(encryptedData, iv);
console.log('Decrypted data:', decryptedData);
};
const appId = process.env.APP_ID;
const sessionKey = 'TjJLWmxUaGdDMmFZYVE2eQ==';
decryptPhoneNumber({ detail: demoData }, { appId, sessionKey });