Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 057e4bfe3a | |||
| 2786542ffd | |||
|
|
a8329f1179 | ||
|
|
031e4823b9 |
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "hello",
|
"name": "totp",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "hello",
|
"name": "totp",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"crypto-js": "^4.2.0"
|
"crypto-js": "^4.2.0"
|
||||||
|
|||||||
@ -1,44 +0,0 @@
|
|||||||
{
|
|
||||||
"accounts": [
|
|
||||||
{
|
|
||||||
"name": "Microsoft",
|
|
||||||
"username": "example@outlook.com",
|
|
||||||
"secret": "JBSWY3DPEHPK3PXP",
|
|
||||||
"enabled": true,
|
|
||||||
"type": "totp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "GitHub",
|
|
||||||
"username": "myusername",
|
|
||||||
"secret": "HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ",
|
|
||||||
"enabled": true,
|
|
||||||
"type": "totp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Steam",
|
|
||||||
"username": "steamuser",
|
|
||||||
"secret": "NBSWY3DPEHPK3PXPNBSWY3DPEHPK3PXP",
|
|
||||||
"enabled": true,
|
|
||||||
"type": "steam"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Work Server",
|
|
||||||
"username": "employee",
|
|
||||||
"secret": "MFRGG2LTEBYGCZLSMUFA====",
|
|
||||||
"enabled": true,
|
|
||||||
"type": "totp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Microsoft",
|
|
||||||
"username": "user@example.com",
|
|
||||||
"secret": "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ",
|
|
||||||
"enabled": false,
|
|
||||||
"type": "totp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"settings": {
|
|
||||||
"title": "TOTP",
|
|
||||||
"refreshInterval": 20,
|
|
||||||
"maxDisplayAccounts": 10
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"package": "com.deepfal.totp.authenticator",
|
"package": "cn.deepfal.band.TOTPauthenticator",
|
||||||
"name": "TOTP",
|
"name": "TOTP",
|
||||||
"versionName": "1.0.0",
|
"versionName": "1.1.0",
|
||||||
"versionCode": 1,
|
"versionCode": 2,
|
||||||
"minPlatformVersion": 1000,
|
"minPlatformVersion": 1000,
|
||||||
"icon": "/common/logo.png",
|
"icon": "/common/logo.png",
|
||||||
"deviceTypeList": [
|
"deviceTypeList": [
|
||||||
@ -14,6 +14,12 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "system.vibrator"
|
"name": "system.vibrator"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "system.interconnect"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "system.storage"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"config": {
|
"config": {
|
||||||
|
|||||||
@ -86,16 +86,20 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { TOTP, SteamTotp } from '../../utils/totp'
|
import { TOTP, SteamTotp } from '../../utils/totp'
|
||||||
import accountsConfig from '../../common/accounts.json'
|
|
||||||
import vibrator from '@system.vibrator'
|
import vibrator from '@system.vibrator'
|
||||||
|
import storage from '@system.storage'
|
||||||
|
import interconnect from '@system.interconnect'
|
||||||
|
|
||||||
// Key class similar to BandTOTP
|
// Key class similar to BandTOTP
|
||||||
class TOTPKey {
|
class TOTPKey {
|
||||||
constructor(key, name, usr = '', type = 'totp') {
|
constructor(key, name, usr = '', type = 'totp', algorithm = 'SHA1', digits = 6, period = 30) {
|
||||||
this.name = name
|
this.name = name
|
||||||
this.usr = usr
|
this.usr = usr
|
||||||
this.key = key
|
this.key = key
|
||||||
this.type = type // 新增类型字段:'totp' 或 'steam'
|
this.type = type // 'totp' 或 'steam'
|
||||||
|
this.algorithm = algorithm
|
||||||
|
this.digits = digits
|
||||||
|
this.period = period
|
||||||
this.update()
|
this.update()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,7 +109,7 @@ class TOTPKey {
|
|||||||
if (this.type === 'steam') {
|
if (this.type === 'steam') {
|
||||||
this.otp = SteamTotp(this.key)
|
this.otp = SteamTotp(this.key)
|
||||||
} else {
|
} else {
|
||||||
this.otp = TOTP(this.key)
|
this.otp = TOTP(this.key, this.algorithm, this.digits, this.period)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.otp = "ERROR"
|
this.otp = "ERROR"
|
||||||
@ -113,6 +117,8 @@ class TOTPKey {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let interconnectConn = null
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
private: {
|
private: {
|
||||||
totpList: [],
|
totpList: [],
|
||||||
@ -130,59 +136,417 @@ export default {
|
|||||||
lastUpdateTime: 0,
|
lastUpdateTime: 0,
|
||||||
// 配置相关
|
// 配置相关
|
||||||
appTitle: "TOTP验证器",
|
appTitle: "TOTP验证器",
|
||||||
refreshInterval: 100
|
refreshInterval: 100,
|
||||||
|
// interconnect状态
|
||||||
|
isConnected: false,
|
||||||
|
connectionStatus: '未连接'
|
||||||
},
|
},
|
||||||
|
|
||||||
onInit() {
|
onInit() {
|
||||||
|
console.log('TOTP应用初始化')
|
||||||
|
|
||||||
// 加载配置文件
|
// 初始化interconnect连接
|
||||||
this.loadAccountsFromConfig()
|
this.initInterconnect()
|
||||||
|
|
||||||
|
// 只从本地存储加载账户(不再从配置文件加载)
|
||||||
|
this.loadAccountsFromStorage()
|
||||||
|
|
||||||
|
// 设置interconnect消息监听
|
||||||
|
this.setupInterconnectListener()
|
||||||
|
|
||||||
// 设置初始颜色
|
// 设置初始颜色
|
||||||
this.updateColors(this.timeLeft)
|
this.updateColors(this.timeLeft)
|
||||||
|
|
||||||
this.startTimer()
|
this.startTimer()
|
||||||
},
|
},
|
||||||
|
|
||||||
loadAccountsFromConfig() {
|
initInterconnect() {
|
||||||
|
console.log('初始化interconnect连接')
|
||||||
try {
|
try {
|
||||||
|
interconnectConn = interconnect.instance()
|
||||||
|
|
||||||
// 加载设置
|
interconnectConn.getReadyState({
|
||||||
if (accountsConfig.settings) {
|
success: data => {
|
||||||
this.appTitle = accountsConfig.settings.title || "TOTP验证器"
|
console.log('interconnect状态:', data.status)
|
||||||
this.refreshInterval = accountsConfig.settings.refreshInterval || 100
|
if (data.status === 1) {
|
||||||
|
this.isConnected = true
|
||||||
|
this.connectionStatus = '已连接'
|
||||||
|
console.log('interconnect连接成功')
|
||||||
|
} else if (data.status === 2) {
|
||||||
|
this.isConnected = false
|
||||||
|
this.connectionStatus = '连接失败'
|
||||||
|
console.log('interconnect连接失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: (data, code) => {
|
||||||
|
console.log('获取interconnect状态失败, code =', code)
|
||||||
|
this.connectionStatus = '状态检查失败'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 在initInterconnect中不设置onmessage,使用单独的方法
|
||||||
|
// 监听消息接收将在setupInterconnectListener中设置
|
||||||
|
|
||||||
|
// 监听连接打开
|
||||||
|
interconnectConn.onopen = () => {
|
||||||
|
console.log('interconnect连接已打开')
|
||||||
|
this.isConnected = true
|
||||||
|
this.connectionStatus = '已连接'
|
||||||
|
|
||||||
|
// 连接成功后,请求当前账户列表给手机
|
||||||
|
this.sendCurrentAccountsToPhone()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听连接关闭
|
||||||
|
interconnectConn.onclose = (data) => {
|
||||||
|
console.log('interconnect连接已关闭, reason =', data.data, ', code =', data.code)
|
||||||
|
this.isConnected = false
|
||||||
|
this.connectionStatus = '连接已关闭'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听连接错误
|
||||||
|
interconnectConn.onerror = (data) => {
|
||||||
|
console.log('interconnect连接错误, errMsg =', data.data, ', errCode =', data.code)
|
||||||
|
this.isConnected = false
|
||||||
|
this.connectionStatus = '连接错误'
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('初始化interconnect失败:', error)
|
||||||
|
this.connectionStatus = '初始化失败'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setupInterconnectListener() {
|
||||||
|
if (!interconnectConn) {
|
||||||
|
console.log('interconnect连接不存在,无法设置监听')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 完全模仿BandTOTP-Band的消息处理逻辑
|
||||||
|
interconnectConn.onmessage = (data) => {
|
||||||
|
try {
|
||||||
|
console.log('接收到手机消息:', data.data)
|
||||||
|
let parsedData = JSON.parse(data.data)
|
||||||
|
|
||||||
|
if (parsedData.list && Array.isArray(parsedData.list)) {
|
||||||
|
parsedData.list.forEach((e) => {
|
||||||
|
// 查找是否已存在相同密钥
|
||||||
|
let index = this.totpList.findIndex((a) => e.key == a.key)
|
||||||
|
|
||||||
|
if (index != -1) {
|
||||||
|
// 密钥重复:只更新名字和用户名
|
||||||
|
this.totpList[index].name = e.name
|
||||||
|
this.totpList[index].usr = e.usr
|
||||||
|
} else {
|
||||||
|
// 新密钥:创建新实例并添加
|
||||||
|
this.totpList.push(new TOTPKey(
|
||||||
|
e.key,
|
||||||
|
e.name,
|
||||||
|
e.usr || '',
|
||||||
|
e.type || 'totp',
|
||||||
|
e.algorithm || 'SHA1',
|
||||||
|
e.digits || 6,
|
||||||
|
e.period || 30
|
||||||
|
))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 关键:立即保存到存储(学习BandTOTP-Band)
|
||||||
|
this.saveAccountsToStorage()
|
||||||
|
|
||||||
|
console.log('成功更新', this.totpList.length, '个账户')
|
||||||
|
|
||||||
|
// 震动反馈表示数据同步成功
|
||||||
|
vibrator.vibrate({
|
||||||
|
mode: 'short'
|
||||||
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
vibrator.vibrate({
|
||||||
|
mode: 'short'
|
||||||
|
})
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理获取账户列表请求
|
||||||
|
if (parsedData.cmd === 'getAccounts') {
|
||||||
|
console.log('手机请求获取账户列表')
|
||||||
|
this.sendCurrentAccountsToPhone()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理删除账户请求
|
||||||
|
if (parsedData.cmd === 'deleteAccount' && parsedData.key) {
|
||||||
|
console.log('手机请求删除账户:', parsedData.key)
|
||||||
|
this.deleteAccountByKey(parsedData.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理重排序账户请求
|
||||||
|
if (parsedData.cmd === 'reorderAccounts' && parsedData.accounts) {
|
||||||
|
console.log('手机请求重排序账户')
|
||||||
|
this.reorderAccounts(parsedData.accounts)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log('处理手机消息失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
sendCurrentAccountsToPhone() {
|
||||||
|
if (!interconnectConn || !this.isConnected) {
|
||||||
|
console.log('interconnect未连接,无法发送账户列表')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 构造与Android app兼容的数据格式
|
||||||
|
const accountsData = {
|
||||||
|
list: this.totpList.map(account => ({
|
||||||
|
name: account.name,
|
||||||
|
usr: account.usr,
|
||||||
|
key: account.key,
|
||||||
|
algorithm: account.algorithm || 'SHA1',
|
||||||
|
digits: account.digits || 6,
|
||||||
|
period: account.period || 30,
|
||||||
|
type: account.type || 'totp'
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载启用的账户
|
console.log('发送账户列表到手机:', accountsData)
|
||||||
const enabledAccounts = accountsConfig.accounts.filter(account => account.enabled)
|
|
||||||
|
|
||||||
// 转换为TOTPKey实例
|
interconnectConn.send({
|
||||||
this.totpList = enabledAccounts.map(account => {
|
data: accountsData,
|
||||||
return new TOTPKey(
|
success: () => {
|
||||||
account.secret,
|
console.log('账户列表发送成功')
|
||||||
account.name,
|
},
|
||||||
account.username,
|
fail: (data) => {
|
||||||
account.type || 'totp' // 如果没有指定type,默认为totp
|
console.log('账户列表发送失败, errMsg =', data.data, ', errCode =', data.code)
|
||||||
)
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.log('构造发送数据失败:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
loadAccountsFromStorage() {
|
||||||
|
storage.get({
|
||||||
|
key: 'keys',
|
||||||
|
success: (data) => {
|
||||||
|
if (data) {
|
||||||
|
console.log('从存储加载账户:', data)
|
||||||
|
try {
|
||||||
|
const parsedData = JSON.parse(data)
|
||||||
|
if (parsedData && parsedData.list && Array.isArray(parsedData.list)) {
|
||||||
|
// 清空现有列表,避免重复加载
|
||||||
|
this.totpList = []
|
||||||
|
|
||||||
|
parsedData.list.forEach((account) => {
|
||||||
|
this.totpList.push(new TOTPKey(
|
||||||
|
account.key,
|
||||||
|
account.name,
|
||||||
|
account.usr || '',
|
||||||
|
account.type || 'totp',
|
||||||
|
account.algorithm || 'SHA1',
|
||||||
|
account.digits || 6,
|
||||||
|
account.period || 30
|
||||||
|
))
|
||||||
|
})
|
||||||
|
console.log('从存储加载了', this.totpList.length, '个账户')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('解析存储数据失败:', error)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果没有存储数据,确保列表为空
|
||||||
|
console.log('存储中没有账户数据')
|
||||||
|
this.totpList = []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: (data, code) => {
|
||||||
|
console.log('从存储加载账户失败:', code)
|
||||||
|
// 加载失败时也确保列表为空,避免使用旧数据
|
||||||
|
this.totpList = []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
saveAccountsToStorage() {
|
||||||
|
const saveData = {
|
||||||
|
list: this.totpList.map(account => ({
|
||||||
|
name: account.name,
|
||||||
|
usr: account.usr,
|
||||||
|
key: account.key,
|
||||||
|
algorithm: account.algorithm || 'SHA1',
|
||||||
|
digits: account.digits || 6,
|
||||||
|
period: account.period || 30,
|
||||||
|
type: account.type || 'totp'
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
storage.set({
|
||||||
|
key: 'keys',
|
||||||
|
value: JSON.stringify(saveData)
|
||||||
|
})
|
||||||
|
console.log('账户保存到存储,数量:', this.totpList.length)
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteAccountByKey(keyToDelete) {
|
||||||
|
const initialCount = this.totpList.length
|
||||||
|
|
||||||
|
// 根据key删除账户
|
||||||
|
this.totpList = this.totpList.filter(account => account.key !== keyToDelete)
|
||||||
|
|
||||||
|
const deletedCount = initialCount - this.totpList.length
|
||||||
|
|
||||||
|
if (deletedCount > 0) {
|
||||||
|
// 保存更新后的账户列表
|
||||||
|
this.saveAccountsToStorage()
|
||||||
|
|
||||||
|
console.log('成功删除账户,删除数量:', deletedCount, '剩余数量:', this.totpList.length)
|
||||||
|
|
||||||
|
// 删除成功震动反馈
|
||||||
|
vibrator.vibrate({
|
||||||
|
mode: 'long'
|
||||||
})
|
})
|
||||||
|
|
||||||
this.totpList.forEach(() => {
|
// 如果正在查看被删除的账户详情,关闭详情页
|
||||||
|
if (this.showDetailPage && this.selectedItem && this.selectedItem.key === keyToDelete) {
|
||||||
|
this.hideDetail()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 向手机发送删除成功确认
|
||||||
|
this.sendDeleteConfirmationToPhone(keyToDelete, true)
|
||||||
|
} else {
|
||||||
|
console.log('未找到要删除的账户:', keyToDelete)
|
||||||
|
|
||||||
|
// 向手机发送删除失败确认
|
||||||
|
this.sendDeleteConfirmationToPhone(keyToDelete, false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
sendDeleteConfirmationToPhone(deletedKey, success) {
|
||||||
|
if (!interconnectConn || !this.isConnected) {
|
||||||
|
console.log('interconnect未连接,无法发送删除确认')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const confirmationData = {
|
||||||
|
cmd: 'deleteConfirmation',
|
||||||
|
key: deletedKey,
|
||||||
|
success: success
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('发送删除确认到手机:', confirmationData)
|
||||||
|
|
||||||
|
interconnectConn.send({
|
||||||
|
data: confirmationData,
|
||||||
|
success: () => {
|
||||||
|
console.log('删除确认发送成功')
|
||||||
|
},
|
||||||
|
fail: (data) => {
|
||||||
|
console.log('删除确认发送失败, errMsg =', data.data, ', errCode =', data.code)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.log('构造删除确认数据失败:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
reorderAccounts(newAccounts) {
|
||||||
|
try {
|
||||||
|
console.log('开始重排序账户,新顺序数量:', newAccounts.length)
|
||||||
|
|
||||||
|
// 根据新顺序重建账户列表
|
||||||
|
const reorderedList = []
|
||||||
|
|
||||||
|
newAccounts.forEach((accountData) => {
|
||||||
|
// 在现有列表中查找匹配的账户
|
||||||
|
const existingAccount = this.totpList.find(account => account.key === accountData.key)
|
||||||
|
|
||||||
|
if (existingAccount) {
|
||||||
|
// 保持现有的TOTPKey实例,只更新顺序
|
||||||
|
reorderedList.push(existingAccount)
|
||||||
|
} else {
|
||||||
|
// 如果没找到,创建新的TOTPKey实例
|
||||||
|
console.log('创建新账户实例:', accountData.name)
|
||||||
|
reorderedList.push(new TOTPKey(
|
||||||
|
accountData.key,
|
||||||
|
accountData.name,
|
||||||
|
accountData.usr || '',
|
||||||
|
accountData.type || 'totp',
|
||||||
|
accountData.algorithm || 'SHA1',
|
||||||
|
accountData.digits || 6,
|
||||||
|
accountData.period || 30
|
||||||
|
))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 更新账户列表
|
||||||
|
this.totpList = reorderedList
|
||||||
|
|
||||||
|
// 保存到存储
|
||||||
|
this.saveAccountsToStorage()
|
||||||
|
|
||||||
|
console.log('账户重排序完成,新顺序数量:', this.totpList.length)
|
||||||
|
|
||||||
|
// 重排序成功震动反馈
|
||||||
|
vibrator.vibrate({
|
||||||
|
mode: 'short'
|
||||||
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
vibrator.vibrate({
|
||||||
|
mode: 'short'
|
||||||
|
})
|
||||||
|
}, 100)
|
||||||
|
|
||||||
|
// 向手机发送重排序成功确认
|
||||||
|
this.sendReorderConfirmationToPhone(true)
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load accounts config:', error)
|
console.log('重排序账户失败:', error)
|
||||||
// 如果加载配置失败,使用默认账户
|
this.sendReorderConfirmationToPhone(false)
|
||||||
this.loadDefaultAccounts()
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
sendReorderConfirmationToPhone(success) {
|
||||||
|
if (!interconnectConn || !this.isConnected) {
|
||||||
|
console.log('interconnect未连接,无法发送重排序确认')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const confirmationData = {
|
||||||
|
cmd: 'reorderConfirmation',
|
||||||
|
success: success
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('发送重排序确认到手机:', confirmationData)
|
||||||
|
|
||||||
|
interconnectConn.send({
|
||||||
|
data: confirmationData,
|
||||||
|
success: () => {
|
||||||
|
console.log('重排序确认发送成功')
|
||||||
|
},
|
||||||
|
fail: (data) => {
|
||||||
|
console.log('重排序确认发送失败, errMsg =', data.data, ', errCode =', data.code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.log('构造重排序确认数据失败:', error)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
loadDefaultAccounts() {
|
// 已移除loadAccountsFromConfig - 只能通过手机端管理账户
|
||||||
},
|
|
||||||
|
|
||||||
onShow() {
|
onShow() {
|
||||||
this.isVisible = true
|
this.isVisible = true
|
||||||
this.updateCodes()
|
this.updateCodes()
|
||||||
this.startTimer()
|
this.startTimer()
|
||||||
|
|
||||||
|
// 页面显示时,如果已连接则发送当前账户列表
|
||||||
|
if (this.isConnected) {
|
||||||
|
this.sendCurrentAccountsToPhone()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onHide() {
|
onHide() {
|
||||||
@ -206,6 +570,15 @@ export default {
|
|||||||
clearInterval(this.updateInterval)
|
clearInterval(this.updateInterval)
|
||||||
this.updateInterval = null
|
this.updateInterval = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清理interconnect连接
|
||||||
|
if (interconnectConn) {
|
||||||
|
try {
|
||||||
|
interconnectConn = null
|
||||||
|
} catch (error) {
|
||||||
|
console.log('清理interconnect连接失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateCodes() {
|
updateCodes() {
|
||||||
@ -343,6 +716,17 @@ export default {
|
|||||||
vibrator.vibrate({
|
vibrator.vibrate({
|
||||||
mode: 'short'
|
mode: 'short'
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
onBackPress() {
|
||||||
|
console.log('触发:onBackPress')
|
||||||
|
// 如果详情页打开,则关闭详情页返回列表
|
||||||
|
if (this.showDetailPage) {
|
||||||
|
this.hideDetail()
|
||||||
|
return true // 自己处理,不执行系统默认返回
|
||||||
|
}
|
||||||
|
// 否则执行系统默认返回行为(退出应用)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -433,4 +817,115 @@ export default {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
transition: color 500ms ease-out;
|
transition: color 500ms ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 详情页面 */
|
||||||
|
.detail-page {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #000;
|
||||||
|
flex-direction: column;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 40px 20px 20px 20px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-service {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-account {
|
||||||
|
font-size: 22px;
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-code-container {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 2px solid #333;
|
||||||
|
height: 120px;
|
||||||
|
width: 170px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-code-row {
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin: 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-code-digit {
|
||||||
|
width: 30px;
|
||||||
|
height: 50px;
|
||||||
|
margin: 0 1px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-code-num {
|
||||||
|
font-size: 32px;
|
||||||
|
color: #00ff88;
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: monospace;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-code-single {
|
||||||
|
font-size: 32px;
|
||||||
|
color: #00ff88;
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: monospace;
|
||||||
|
letter-spacing: 3px;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-time {
|
||||||
|
font-size: 22px;
|
||||||
|
color: #AAA;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 底部按钮容器 */
|
||||||
|
.detail-footer {
|
||||||
|
height: 100px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-back-btn {
|
||||||
|
width: 70px;
|
||||||
|
height: 70px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
|
border-radius: 35px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border: 2px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-back-icon {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -3,9 +3,9 @@ let timeOffset=0
|
|||||||
function time(){
|
function time(){
|
||||||
return Math.floor(Date.now() / 1000) + (timeOffset || 0);
|
return Math.floor(Date.now() / 1000) + (timeOffset || 0);
|
||||||
}
|
}
|
||||||
function TOTP(key) {
|
function TOTP(key, algorithm = 'SHA1', digits = 6, period = 30) {
|
||||||
|
|
||||||
const timeStep = 30; // 默认时间步长为 30 秒
|
const timeStep = period; // 使用传入的时间周期
|
||||||
const currentTime = time()
|
const currentTime = time()
|
||||||
const timeCounter = Math.floor(currentTime / timeStep).toString(16).padStart(16, '0'); // 时间计数器,16位
|
const timeCounter = Math.floor(currentTime / timeStep).toString(16).padStart(16, '0'); // 时间计数器,16位
|
||||||
|
|
||||||
@ -13,8 +13,20 @@ function TOTP(key) {
|
|||||||
// 使用 Base32 解码并返回 WordArray
|
// 使用 Base32 解码并返回 WordArray
|
||||||
const decodedKey = base32Decode(key);
|
const decodedKey = base32Decode(key);
|
||||||
|
|
||||||
// 使用 HMAC-SHA1 计算哈希值
|
// 根据算法选择不同的HMAC函数
|
||||||
const hmacHash = CryptoJS.HmacSHA1(CryptoJS.enc.Hex.parse(timeCounter),decodedKey);
|
let hmacHash;
|
||||||
|
switch (algorithm.toUpperCase()) {
|
||||||
|
case 'SHA256':
|
||||||
|
hmacHash = CryptoJS.HmacSHA256(CryptoJS.enc.Hex.parse(timeCounter), decodedKey);
|
||||||
|
break;
|
||||||
|
case 'SHA512':
|
||||||
|
hmacHash = CryptoJS.HmacSHA512(CryptoJS.enc.Hex.parse(timeCounter), decodedKey);
|
||||||
|
break;
|
||||||
|
case 'SHA1':
|
||||||
|
default:
|
||||||
|
hmacHash = CryptoJS.HmacSHA1(CryptoJS.enc.Hex.parse(timeCounter), decodedKey);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// 动态截取
|
// 动态截取
|
||||||
const hmacBytes = hexToBytes(hmacHash.toString());
|
const hmacBytes = hexToBytes(hmacHash.toString());
|
||||||
@ -25,8 +37,9 @@ function TOTP(key) {
|
|||||||
(hmacBytes[offset + 2] << 8) |
|
(hmacBytes[offset + 2] << 8) |
|
||||||
hmacBytes[offset + 3];
|
hmacBytes[offset + 3];
|
||||||
|
|
||||||
// 生成 6 位 OTP
|
// 生成指定位数的OTP
|
||||||
const result = (otp % 1000000).toString().padStart(6, '0');
|
const modulo = Math.pow(10, digits);
|
||||||
|
const result = (otp % modulo).toString().padStart(digits, '0');
|
||||||
return result
|
return result
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error
|
throw error
|
||||||
@ -77,7 +90,7 @@ function bufferizeSecret(secret) {
|
|||||||
if (secret.match(/^[0-9a-f]{40}$/i)) {
|
if (secret.match(/^[0-9a-f]{40}$/i)) {
|
||||||
return CryptoJS.enc.Hex.parse(secret); // 解析为 WordArray
|
return CryptoJS.enc.Hex.parse(secret); // 解析为 WordArray
|
||||||
} else {
|
} else {
|
||||||
// 假设是 base32 编码
|
// 假设是 base32 编码(包括Steam的标准secret)
|
||||||
return base32Decode(secret); // 解析为 WordArray
|
return base32Decode(secret); // 解析为 WordArray
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user