diff --git a/.gitignore b/.gitignore index b512c09..a2e04e4 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -node_modules \ No newline at end of file +node_modules + +.env \ No newline at end of file diff --git a/packages/decrypt-phone-by-data/.env.example b/packages/decrypt-phone-by-data/.env.example new file mode 100644 index 0000000..bf46132 --- /dev/null +++ b/packages/decrypt-phone-by-data/.env.example @@ -0,0 +1,3 @@ +APP_ID=68c47321131b9b0001166b7d +APP_SECRET=a9f02a4bef2e29b2b6f866d9d2aff2e6 +CODE=ecd82af4267247b1a1a59b6df7fdd320 \ No newline at end of file diff --git a/packages/decrypt-phone-by-data/demo.json b/packages/decrypt-phone-by-data/demo.json deleted file mode 100644 index 58b2f27..0000000 --- a/packages/decrypt-phone-by-data/demo.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "encryptedData": "NpiIHuKkhY2S8pCG9WShtDcCLmBhnKkU+m0BLUHJKWkUFrkPay6j3Lc7wkzKat8U/gWsP2WfcMHS6nz48VvjyyKAeXrAxQKEFIxLKslRqntEJnvu5HmANssOypHHk7Y7ovij1QvY+Od/pTBKL73i2AzLoNtXubMTWSPcqG0A/Ov1uy4U7zRZpUIa5otQ+o5dqHc4+rj5EnT2MQ73+QGcFA==", - "errMsg": "getPhoneNumber:ok", - "iv": "aUFhbEhJbWFydjVNQlE4cg==" -} \ No newline at end of file diff --git a/packages/decrypt-phone-by-data/demo.ts b/packages/decrypt-phone-by-data/demo.ts new file mode 100644 index 0000000..aec2fbf --- /dev/null +++ b/packages/decrypt-phone-by-data/demo.ts @@ -0,0 +1,6 @@ +export const demoData = { + encryptedData: + 'HdZv6GsUqB68aoLf+SqSaGSqffOTMHHW89DTjh7suCIz9gsdK0wUTpL8+60+zqKcL3b1jNxrRdmZxXGHO67eH1HG5WSoJU7Jlu1WjUWzK9ZhBuPJNMP7Z0aHx9aPOyk1grlZNHOA51/i+AlKywm9QY3RLRTtPQpoSTcjJud1iWhF+aYBrI8pS/rw/AYQuRvOuODjQXinyAC6pJPqZzfs5w==', + errMsg: 'getPhoneNumber:ok', + iv: 'N2dPdU9wT0k3bnNKZlhsZw==', +}; diff --git a/packages/decrypt-phone-by-data/package.json b/packages/decrypt-phone-by-data/package.json index e860c80..1316066 100644 --- a/packages/decrypt-phone-by-data/package.json +++ b/packages/decrypt-phone-by-data/package.json @@ -10,5 +10,10 @@ "author": "abearxiong (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" + } } diff --git a/packages/decrypt-phone-by-data/src/decrypt.ts b/packages/decrypt-phone-by-data/src/decrypt.ts new file mode 100644 index 0000000..d873b0c --- /dev/null +++ b/packages/decrypt-phone-by-data/src/decrypt.ts @@ -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 : '')); + } + } +} diff --git a/packages/decrypt-phone-by-data/src/get-session-key.ts b/packages/decrypt-phone-by-data/src/get-session-key.ts new file mode 100644 index 0000000..f8ae742 --- /dev/null +++ b/packages/decrypt-phone-by-data/src/get-session-key.ts @@ -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); diff --git a/packages/decrypt-phone-by-data/src/index.ts b/packages/decrypt-phone-by-data/src/index.ts new file mode 100644 index 0000000..86b52f0 --- /dev/null +++ b/packages/decrypt-phone-by-data/src/index.ts @@ -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 }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..6e0eedc --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,48 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: {} + + packages/decrypt-phone-by-data: + devDependencies: + '@types/node': + specifier: ^24.3.3 + version: 24.3.3 + crypto-js: + specifier: ^4.2.0 + version: 4.2.0 + dotenv: + specifier: ^17.2.2 + version: 17.2.2 + +packages: + + '@types/node@24.3.3': + resolution: {integrity: sha512-GKBNHjoNw3Kra1Qg5UXttsY5kiWMEfoHq2TmXb+b1rcm6N7B3wTrFYIf/oSZ1xNQ+hVVijgLkiDZh7jRRsh+Gw==} + + crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + + dotenv@17.2.2: + resolution: {integrity: sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==} + engines: {node: '>=12'} + + undici-types@7.10.0: + resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==} + +snapshots: + + '@types/node@24.3.3': + dependencies: + undici-types: 7.10.0 + + crypto-js@4.2.0: {} + + dotenv@17.2.2: {} + + undici-types@7.10.0: {}