Compare commits
6 Commits
00371b1dda
...
19b8b92f49
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19b8b92f49 | ||
|
|
f584b49cd4 | ||
|
|
13eefdd50c | ||
|
|
3cd9031eef | ||
|
|
94c901a233 | ||
|
|
1819c31267 |
@@ -131,33 +131,37 @@ export default class LingChairClient {
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 客户端上线
|
||||
* 进行身份验证以接受客户端事件
|
||||
*
|
||||
* 使用验证方式优先级: 访问 > 刷新 > 账号密码
|
||||
*
|
||||
* 不会逐一尝试
|
||||
* 若传递了账号密码, 则同时缓存新的访问令牌和刷新令牌
|
||||
*
|
||||
* 如只传递两个令牌的其一, 按照优先级并在成功验证后赋值
|
||||
*
|
||||
* 多个验证方式不会逐一尝试
|
||||
*/
|
||||
async authOrThrow(args: {
|
||||
refresh_token?: string,
|
||||
access_token?: string,
|
||||
account?: string,
|
||||
password?: string,
|
||||
refresh_token?: string
|
||||
access_token?: string
|
||||
account?: string
|
||||
password?: string
|
||||
ignore_all_empty?: boolean
|
||||
}) {
|
||||
if ((!args.access_token && !args.refresh_token) && (!args.account && !args.password))
|
||||
throw new Error('Access/Refresh token or account & password required')
|
||||
if ((!args.access_token && !args.refresh_token) && (!args.account && !args.password) && !args.ignore_all_empty)
|
||||
throw new Error('Access/Refresh token or account & password required, or ignore_all_empty=true')
|
||||
|
||||
this.auth_cache = args
|
||||
|
||||
this.refresh_token = args.refresh_token
|
||||
|
||||
let access_token = args.access_token
|
||||
if (!access_token && args.refresh_token) {
|
||||
const re = await this.invoke('User.refreshAccessToken', {
|
||||
refresh_token: args.refresh_token,
|
||||
})
|
||||
if (re.code == 200)
|
||||
if (re.code == 200) {
|
||||
access_token = re.data!.access_token as string | undefined
|
||||
else
|
||||
this.refresh_token = args.refresh_token
|
||||
} else
|
||||
throw new CallbackError(re)
|
||||
}
|
||||
|
||||
@@ -166,9 +170,10 @@ export default class LingChairClient {
|
||||
account: args.account,
|
||||
password: crypto.createHash('sha256').update(args.password).digest('hex'),
|
||||
})
|
||||
if (re.code == 200)
|
||||
if (re.code == 200) {
|
||||
access_token = re.data!.access_token as string | undefined
|
||||
else
|
||||
this.refresh_token = re.data!.refresh_token as string
|
||||
} else
|
||||
throw new CallbackError(re)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,43 +1,56 @@
|
||||
// @ts-types="npm:@types/crypto-js"
|
||||
import * as CryptoJS from 'crypto-js'
|
||||
import crypto from 'node:crypto'
|
||||
|
||||
const dataIsEmpty = !localStorage.tws_data || localStorage.tws_data == ''
|
||||
|
||||
const aes = {
|
||||
enc: (data: string, key: string) => CryptoJS.AES.encrypt(data, key).toString(),
|
||||
dec: (data: string, key: string) => CryptoJS.AES.decrypt(data, key).toString(CryptoJS.enc.Utf8),
|
||||
class Aes {
|
||||
static randomIv() {
|
||||
return crypto.randomBytes(12)
|
||||
}
|
||||
static normalizeKey(key: string, keyLength = 32) {
|
||||
const hash = crypto.createHash('sha256')
|
||||
hash.update(key)
|
||||
const keyBuffer = hash.digest()
|
||||
return keyLength ? keyBuffer.subarray(0, keyLength) : keyBuffer
|
||||
}
|
||||
static encrypt(data: string, key: string) {
|
||||
const iv = this.randomIv()
|
||||
return Buffer.concat([iv, crypto.createCipheriv("aes-256-gcm", this.normalizeKey(key), iv).update(data)]).toString('hex')
|
||||
}
|
||||
static decrypt(data: string, key: string) {
|
||||
const buffer = Buffer.from(data, 'hex')
|
||||
const iv = buffer.subarray(0, 12)
|
||||
return crypto.createDecipheriv("aes-256-gcm", this.normalizeKey(key), iv).update(buffer.subarray(12)).toString()
|
||||
}
|
||||
}
|
||||
|
||||
const key = location.host + '_TWS_姐姐'
|
||||
// 尽可能防止被窃取, 虽然理论上还是会被窃取
|
||||
const key = crypto.createHash('sha256').update(location.host + '_TWS_姐姐_' + navigator.userAgent).digest().toString('base64')
|
||||
|
||||
if (dataIsEmpty) localStorage.tws_data = aes.enc('{}', key)
|
||||
if (dataIsEmpty) localStorage.tws_data = Aes.encrypt('{}', key)
|
||||
|
||||
let _dec = aes.dec(localStorage.tws_data, key)
|
||||
let _dec = Aes.decrypt(localStorage.tws_data, key)
|
||||
if (_dec == '') _dec = '{}'
|
||||
|
||||
const _data_cached = JSON.parse(_dec)
|
||||
|
||||
// 類型定義
|
||||
declare global {
|
||||
interface Window {
|
||||
data: {
|
||||
type IData = {
|
||||
refresh_token?: string
|
||||
split_sizes: number[]
|
||||
apply(): void
|
||||
access_token?: string
|
||||
device_id: string
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
data?: IData
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore: 忽略...
|
||||
// deno-lint-ignore no-window
|
||||
(window.data == null) && (window.data = new Proxy({
|
||||
apply() {}
|
||||
}, {
|
||||
const data = new Proxy({} as IData, {
|
||||
get(_obj, k) {
|
||||
if (k == '_cached') return _data_cached
|
||||
if (k == 'apply') return () => localStorage.tws_data = aes.enc(JSON.stringify(_data_cached), key)
|
||||
if (k == 'apply') return () => localStorage.tws_data = Aes.encrypt(JSON.stringify(_data_cached), key)
|
||||
return _data_cached[k]
|
||||
},
|
||||
set(_obj, k, v) {
|
||||
@@ -45,7 +58,11 @@ declare global {
|
||||
_data_cached[k] = v
|
||||
return true
|
||||
}
|
||||
}))
|
||||
})
|
||||
|
||||
// deno-lint-ignore no-window
|
||||
export default window.data
|
||||
if (new URL(location.href).searchParams.get('export_data') == 'true') {
|
||||
window.data = data
|
||||
console.warn("警告: 将 data 暴露到 window 有可能会导致令牌泄露!")
|
||||
}
|
||||
|
||||
export default data
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { LingChairClient } from 'lingchair-client-protocol'
|
||||
import data from "./Data.ts"
|
||||
import data from "./data.ts"
|
||||
import { UAParser } from 'ua-parser-js'
|
||||
import { randomUUID } from 'lingchair-internal-shared'
|
||||
import performAuth from './performAuth.ts'
|
||||
|
||||
if (!data.device_id) {
|
||||
const ua = new UAParser(navigator.userAgent)
|
||||
@@ -12,6 +13,11 @@ const client = new LingChairClient({
|
||||
device_id: data.device_id,
|
||||
auto_fresh_token: true,
|
||||
})
|
||||
try {
|
||||
await performAuth({})
|
||||
} catch (_) {
|
||||
console.log(_)
|
||||
}
|
||||
|
||||
export default function getClient() {
|
||||
return client
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
"build-watch": "npx vite --watch build"
|
||||
},
|
||||
"dependencies": {
|
||||
"crypto-browserify": "3.12.1",
|
||||
"crypto-js": "4.2.0",
|
||||
"dompurify": "3.2.7",
|
||||
"lingchair-internal-shared": "*",
|
||||
"marked": "16.3.0",
|
||||
@@ -26,7 +24,7 @@
|
||||
"@types/react-dom": "18.3.1",
|
||||
"@vitejs/plugin-react": "4.7.0",
|
||||
"chalk": "5.4.1",
|
||||
"vite": "7.0.6",
|
||||
"vite": "7.2.6",
|
||||
"vite-plugin-node-polyfills": "^0.24.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import data from "./Data.ts";
|
||||
import data from "./data.ts"
|
||||
import getClient from "./getClient.ts"
|
||||
|
||||
/**
|
||||
* 尝试进行验证
|
||||
* 进行身份验证以接受客户端事件
|
||||
*
|
||||
* 成功后自动保存到本地
|
||||
* 使用验证方式优先级: 访问 > 刷新 > 账号密码
|
||||
*
|
||||
* 优先级: 账号密码 > 提供刷新令牌 > 储存的刷新令牌
|
||||
* 若传递了账号密码, 则同时缓存新的访问令牌和刷新令牌
|
||||
*
|
||||
* 不会逐一尝试
|
||||
* 如只传递两个令牌的其一, 按照优先级并在成功验证后赋值
|
||||
*
|
||||
* 多个验证方式不会逐一尝试
|
||||
*/
|
||||
export default async function performAuth(args: {
|
||||
refresh_token?: string
|
||||
@@ -21,7 +23,7 @@ export default async function performAuth(args: {
|
||||
password: args.password,
|
||||
})
|
||||
else {
|
||||
await getClient().authOrThrow({ refresh_token: args.refresh_token ? args.refresh_token : data.refresh_token })
|
||||
await getClient().authOrThrow({ refresh_token: args.refresh_token ? args.refresh_token : data.refresh_token, ignore_all_empty: true })
|
||||
}
|
||||
data.refresh_token = getClient().getCachedRefreshToken()
|
||||
data.access_token = getClient().getCachedAccessToken()
|
||||
|
||||
@@ -8,12 +8,11 @@ import MainSharedContext from '../MainSharedContext.ts'
|
||||
export default function LoginDialog({ ...props }: { open: boolean } & React.HTMLAttributes<Dialog>) {
|
||||
const shared = React.useContext(MainSharedContext)
|
||||
|
||||
const loginDialogRef = React.useRef<Dialog>(null)
|
||||
const loginInputAccountRef = React.useRef<TextField>(null)
|
||||
const loginInputPasswordRef = React.useRef<TextField>(null)
|
||||
|
||||
return (
|
||||
<mdui-dialog {...props} headline="登录" ref={loginDialogRef}>
|
||||
<mdui-dialog {...props} headline="登录">
|
||||
|
||||
<mdui-text-field label="用户 ID / 用户名" ref={loginInputAccountRef}></mdui-text-field>
|
||||
<div style={{
|
||||
|
||||
Reference in New Issue
Block a user