diff --git a/entry/src/main/ets/dialogs/DropFilesDialog.ets b/entry/src/main/ets/dialogs/DropFilesDialog.ets index 94661d4..fd23586 100644 --- a/entry/src/main/ets/dialogs/DropFilesDialog.ets +++ b/entry/src/main/ets/dialogs/DropFilesDialog.ets @@ -11,12 +11,9 @@ export struct DropFilesDialog { private appCtx = AppStorage.get('appContext') as common.UIAbilityContext; build() { Column({ space: 10}) { - Row() - { - Row() - { - Column() - { + Row() { + Row() { + Column() { SymbolGlyph(this.replaceFlag ? $r('sys.symbol.chevron_down_2_circle') : $r('sys.symbol.arrow_down_circle')) .fontWeight(FontWeight.Lighter) .fontSize(96) @@ -26,7 +23,6 @@ export struct DropFilesDialog { .fontSize(20) .fontColor($r('app.color.str_gray')) } - } .width('100%') .height('40%') @@ -35,7 +31,7 @@ export struct DropFilesDialog { .onDrop((event?: DragEvent, extraParams?: string) => { console.log("enter onDrop") if (event!=null) { - let records: Array = event.getData().getRecords(); + let records: Array= event.getData().getRecords(); let plainfile: unifiedDataChannel.File = records[0] as unifiedDataChannel.File; this.appCtx.eventHub.emit('onFileDrop',plainfile.uri); } diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index c2ff00d..a0d93fc 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -6,7 +6,7 @@ import { FortiConfigDialog } from '../dialogs/FortiConfigDialog'; import { scanCore, scanBarcode } from '@kit.ScanKit'; import { TokenItem } from '../components/TokenItem'; import { SettingPage } from '../pages/SettingPage' -import { HashMap, util } from '@kit.ArkTS'; +import { util } from '@kit.ArkTS'; import { fileIo as fs } from '@kit.CoreFileKit'; import { BusinessError } from '@kit.BasicServicesKit'; import { router } from '@kit.ArkUI'; @@ -20,27 +20,14 @@ import CommonEventManager from '@ohos.commonEventManager'; import { TokenStore } from '../utils/TokenStore'; import { PermissionManager } from '../utils/PermissionManager'; import { DropFilesDialog } from '../dialogs/DropFilesDialog'; -import { unifiedDataChannel, uniformTypeDescriptor } from '@kit.ArkData'; +import { uniformTypeDescriptor } from '@kit.ArkData'; import { EncryptionPassWordDialog } from '../dialogs/EncryptionPassWordDialog'; -import { faceDetector } from '@kit.CoreVisionKit'; -import MediaFileUri from '../utils/MediaFileUri'; -import { cryptoFramework } from '@kit.CryptoArchitectureKit'; -import { - base32Decode, - base32Encode, - stringToIntArray, - intArrayToString, - generateFileNameWithDate -} from '../utils/TokenUtils' +import { restoreFromBackup } from '../utils/TokenBackup'; const TAG = 'PrivacySubscribe'; let tkStore = TokenStore.getInstance(); -class backup_file { - magic!: number; - version!: number; - configs!: Array; -} + @Entry @ComponentV2 struct Index { @@ -71,10 +58,8 @@ struct Index { }; //拖拽文件弹框 private DropFilesDialogController: CustomDialogController | null = new CustomDialogController({ - builder: DropFilesDialog({ - }), - cancel: () => { - }, + builder: DropFilesDialog({}), + cancel: () => {}, autoCancel: true, openAnimation:({ delay: 0, @@ -178,37 +163,39 @@ struct Index { } }) - this.appCtx.eventHub.on('onFileDrop',( plainfile:string)=>{ - - this.backup_select_uri = plainfile; - if (this.backup_select_uri.endsWith('.bak')) { - //后缀bak为加密包 唤醒加密导入弹窗 - this.dialogController?.open(); + this.appCtx.eventHub.on('onFileDrop', (uri: string) => { + this.backup_select_uri = uri; + if (this.backup_select_uri.endsWith('.bak')) { // 后缀bak为加密包 唤醒加密导入弹窗 + this.password_input_dialog?.open(); } else if (this.backup_select_uri.endsWith('.json')) { - this.readContentFromSelectedFile(this.backup_select_uri); + restoreFromBackup(uri).then((backup) => { + this.updateTokenConfigs(backup.configs).then(() => { + tkStore.sortTokens(); + }); + }); } - setTimeout(()=>{ - this.DropFilesDialogIsOpen=false; + setTimeout(() => { + this.DropFilesDialogIsOpen = false; this.DropFilesDialogController?.close(); - },250) + }, 250) }); } //region 导入 - private backup_magic: number = 0x55aaeebb; - private mediaFileUri: MediaFileUri = new MediaFileUri(); - private dialogController: CustomDialogController | null = new CustomDialogController({ + private password_input_dialog: CustomDialogController | null = new CustomDialogController({ builder: EncryptionPassWordDialog({ - cancel: () => { - }, + cancel: () => {}, confirm: (password: string) => { - this.readContentFromSelectedFileEncryption(this.backup_select_uri, password); + restoreFromBackup(this.backup_select_uri, true, password).then((backup) => { + this.updateTokenConfigs(backup.configs).then(() => { + tkStore.sortTokens(); + }); + }); }, textValue: "", inputValue: "" }), - cancel: () => { - }, + cancel: () => {}, autoCancel: true, onWillDismiss: (dismissDialogAction: DismissDialogAction) => { console.info("reason=" + JSON.stringify(dismissDialogAction.reason)) @@ -226,71 +213,6 @@ struct Index { customStyle: false, cornerRadius: 10, }) - // 加密导入 - async readContentFromSelectedFileEncryption(myUri: string, password: string): Promise { - let backup_string = this.mediaFileUri.readFileContent(myUri); - try { - this.decryptFile(backup_string, password).then((backup_string) => { - let backup: backup_file = JSON.parse(backup_string); - if (backup.magic == this.backup_magic) { - this.updateTokenConfigs(backup.configs).then(()=>{ - tkStore.sortTokens(); - }); - } else { - promptAction.showToast({ message: 'Error: invalid backup file!' }) - } - }) - } catch (err) { - promptAction.showToast({ message: err.message }) - } - - } - async decryptFile(file_enc: string, user_key: string): Promise { - let aes_decoder = cryptoFramework.createCipher('AES128|CBC|PKCS7'); - let aes_iv: cryptoFramework.IvParamsSpec = { - algName: "IvParamsSpec", - iv: { data: stringToIntArray('ohtotptokenaesiv') } - }; - let spec: cryptoFramework.PBKDF2Spec = { - algName: 'PBKDF2', - password: user_key, - salt: new Uint8Array(16), - iterations: 10000, - keySize: 16 - }; - let kdf = cryptoFramework.createKdf('PBKDF2|SHA256'); - let symKeyBlob = kdf.generateSecretSync(spec); - let aesGenerator = cryptoFramework.createSymKeyGenerator('AES128'); - let aes_key = aesGenerator.convertKeySync(symKeyBlob); - await aes_decoder.init(cryptoFramework.CryptoMode.DECRYPT_MODE, aes_key, aes_iv).catch((reason: string) => { - promptAction.showToast({ message: reason }); - }); - return new Promise((resolve) => { - let file_buf = base32Decode(file_enc); - aes_decoder.doFinal({ data: file_buf }).then((decryptData) => { - let decoder = new util.TextDecoder() - resolve(decoder.decodeToString(decryptData.data)); - }).catch((reason: BusinessError) => { - promptAction.showToast({ message: reason.message }); - }); - }); - } - async readContentFromSelectedFile(myUri: string): Promise { - let backup_string = this.mediaFileUri.readFileContent(myUri); - try { - let backup: backup_file = JSON.parse(intArrayToString(base32Decode(backup_string).buffer as ArrayBuffer)); - if (backup.magic == this.backup_magic) { - this.updateTokenConfigs(backup.configs).then(()=>{ - tkStore.sortTokens(); - }); - } else { - promptAction.showToast({ message: 'Error: invalid backup file!' }) - } - } catch (err) { - promptAction.showToast({ message: err.message }) - } - - } //endregion async onTokenChanged(): Promise { diff --git a/entry/src/main/ets/pages/SettingPage.ets b/entry/src/main/ets/pages/SettingPage.ets index 53b887a..0b201af 100644 --- a/entry/src/main/ets/pages/SettingPage.ets +++ b/entry/src/main/ets/pages/SettingPage.ets @@ -1,36 +1,19 @@ import { TokenConfig } from '../utils/TokenConfig'; -import { picker } from '@kit.CoreFileKit'; import { BusinessError } from '@kit.BasicServicesKit'; import { bundleManager, common } from '@kit.AbilityKit'; -import { - base32Decode, - base32Encode, - stringToIntArray, - intArrayToString, - generateFileNameWithDate -} from '../utils/TokenUtils' +import { generateFileNameWithDate } from '../utils/TokenUtils' import promptAction from '@ohos.promptAction' -import Logger from '../utils/Logger'; -import MediaFileUri from '../utils/MediaFileUri'; -import { cryptoFramework } from '@kit.CryptoArchitectureKit'; -import { buffer, util } from '@kit.ArkTS'; import { SubItemToggle } from '../components/SubItemToggle' import { ItemDescription } from '../components/ItemDescription' -import { Content, router } from '@kit.ArkUI'; +import { router } from '@kit.ArkUI'; import { AppPreference } from '../utils/AppPreference'; import { SubItemButton } from '../components/SubItemButton'; import { SettingItem } from '../components/SettingItem'; import { userAuth } from '@kit.UserAuthenticationKit'; import { PermissionManager } from '../utils/PermissionManager'; import { EncryptionPassWordDialog } from '../dialogs/EncryptionPassWordDialog'; -import { faceDetector } from '@kit.CoreVisionKit'; -import { unifiedDataChannel, uniformTypeDescriptor } from '@kit.ArkData'; - -class backup_file { - magic!: number; - version!: number; - configs!: Array; -} +import { restoreFromBackup, saveBackupToFile, TokenBackup } from '../utils/TokenBackup'; +import { showSaveFilePicker, showSelectFilePicker } from '../utils/FileUtils'; @Preview @ComponentV2 @@ -47,10 +30,7 @@ export struct SettingPage { @Local Is_loaded: boolean = false; //备份时是否为导出模式 private backup_is_input: boolean = true; - private backup_magic: number = 0x55aaeebb; - private backup_save_uri: string = ''; private backup_select_uri: string = ''; - private mediaFileUri: MediaFileUri = new MediaFileUri(); private appCtx = AppStorage.get('appContext') as common.UIAbilityContext; private versionCode?: number; private str_about_app: string = ''; @@ -90,36 +70,29 @@ export struct SettingPage { } //region 加密导入导出 - private dialogController: CustomDialogController | null = new CustomDialogController({ + private password_input_dialog: CustomDialogController | null = new CustomDialogController({ builder: EncryptionPassWordDialog({ - cancel: () => { - }, + cancel: () => {}, confirm: (password: string) => { if (this.backup_is_input) { // 如果是导出 就要调用导出加密函数 - const documentSaveOptions = new picker.DocumentSaveOptions(); - documentSaveOptions.newFileNames = [`totp_backup_${generateFileNameWithDate()}.bak`]; - documentSaveOptions.fileSuffixChoices = ['BAK|.bak', '.bak']; - let context = getContext(this) as common.Context; - const documentViewPicker = new picker.DocumentViewPicker(context); - documentViewPicker.save(documentSaveOptions).then((documentSaveResult: Array) => { - if (documentSaveResult !== null && documentSaveResult !== undefined && documentSaveResult.length == 1) { - this.backup_save_uri = documentSaveResult[0]; - Logger.info('documentViewPicker.save to file succeed and uris are:' + documentSaveResult); - this.writeContentForSaveAsFileEncryption(this.backup_save_uri, password); - } - }).catch((err: BusinessError) => { - Logger.error(`Invoke documentViewPicker.save failed, code is ${err.code}, message is ${err.message}`); - }) - } else { - // 加密导入 - this.readContentFromSelectedFileEncryption(this.backup_select_uri, password); + showSaveFilePicker([`totp_backup_${generateFileNameWithDate()}.bak`], ['BAK|.bak']).then((uris) => { + const backup: TokenBackup = new TokenBackup(this.versionCode ?? 0, this.arrConf); + saveBackupToFile(uris[0], backup, true, password).then(() => { + promptAction.showToast({ message: "Backup Success" }); + }).catch((reason: BusinessError) => { + promptAction.showToast({ message: reason.message }); + }); + }); + } else { // 加密导入 + restoreFromBackup(this.backup_select_uri, true, password).then((backup) => { + this.backupReload(backup.configs); + }); } }, textValue: "", inputValue: "" }), - cancel: () => { - }, + cancel: () => {}, autoCancel: true, onWillDismiss: (dismissDialogAction: DismissDialogAction) => { console.info("reason=" + JSON.stringify(dismissDialogAction.reason)) @@ -138,115 +111,7 @@ export struct SettingPage { cornerRadius: 10, }) - async writeContentForSaveAsFile(myUri: string): Promise { - let backup: backup_file = { magic: this.backup_magic, version: this.versionCode ?? 0, configs: this.arrConf }; - let backup_string = base32Encode(stringToIntArray(JSON.stringify(backup))); - this.mediaFileUri.writeFileContent(myUri, backup_string); - promptAction.showToast({ message: `BackUp Success.` }) - } - - // 加密导出 - async writeContentForSaveAsFileEncryption(myUri: string, password: string): Promise { - let backup: backup_file = { magic: this.backup_magic, version: this.versionCode ?? 0, configs: this.arrConf }; - this.encryptFile(JSON.stringify(backup), password).then((backup_string) => { - this.mediaFileUri.writeFileContent(myUri, backup_string); - promptAction.showToast({ message: `BackUp Success.` }) - }); - } - - async readContentFromSelectedFile(myUri: string): Promise { - let backup_string = this.mediaFileUri.readFileContent(myUri); - try { - let backup: backup_file = JSON.parse(intArrayToString(base32Decode(backup_string).buffer as ArrayBuffer)); - if (backup.magic == this.backup_magic) { - this.backupReload(backup.configs); - } else { - promptAction.showToast({ message: 'Error: invalid backup file!' }) - } - } catch (err) { - promptAction.showToast({ message: err.message }) - } - - } - - async decryptFile(file_enc: string, user_key: string): Promise { - let aes_decoder = cryptoFramework.createCipher('AES128|CBC|PKCS7'); - let aes_iv: cryptoFramework.IvParamsSpec = { - algName: "IvParamsSpec", - iv: { data: stringToIntArray('ohtotptokenaesiv') } - }; - let spec: cryptoFramework.PBKDF2Spec = { - algName: 'PBKDF2', - password: user_key, - salt: new Uint8Array(16), - iterations: 10000, - keySize: 16 - }; - let kdf = cryptoFramework.createKdf('PBKDF2|SHA256'); - let symKeyBlob = kdf.generateSecretSync(spec); - let aesGenerator = cryptoFramework.createSymKeyGenerator('AES128'); - let aes_key = aesGenerator.convertKeySync(symKeyBlob); - await aes_decoder.init(cryptoFramework.CryptoMode.DECRYPT_MODE, aes_key, aes_iv).catch((reason: string) => { - promptAction.showToast({ message: reason }); - }); - return new Promise((resolve) => { - let file_buf = base32Decode(file_enc); - aes_decoder.doFinal({ data: file_buf }).then((decryptData) => { - let decoder = new util.TextDecoder() - resolve(decoder.decodeToString(decryptData.data)); - }).catch((reason: BusinessError) => { - promptAction.showToast({ message: reason.message }); - }); - }); - } - - async encryptFile(file_str: string, user_key: string): Promise { - let aes_encoder = cryptoFramework.createCipher('AES128|CBC|PKCS7'); - let aes_iv: cryptoFramework.IvParamsSpec = { - algName: "IvParamsSpec", - iv: { data: stringToIntArray('ohtotptokenaesiv') } - }; - let spec: cryptoFramework.PBKDF2Spec = { - algName: 'PBKDF2', - password: user_key, - salt: new Uint8Array(16), - iterations: 10000, - keySize: 16 - }; - let kdf = cryptoFramework.createKdf('PBKDF2|SHA256'); - let symKeyBlob = kdf.generateSecretSync(spec); - let aesGenerator = cryptoFramework.createSymKeyGenerator('AES128'); - let aes_key = aesGenerator.convertKeySync(symKeyBlob); - aes_encoder.initSync(cryptoFramework.CryptoMode.ENCRYPT_MODE, aes_key, aes_iv); - return new Promise((resolve) => { - aes_encoder.doFinal({ data: new Uint8Array(buffer.from(file_str).buffer) }).then((encryptData) => { - resolve(base32Encode(encryptData.data)); - }).catch((reason: BusinessError) => { - promptAction.showToast({ message: reason.message }); - }); - }); - } - - // 加密导入 - async readContentFromSelectedFileEncryption(myUri: string, password: string): Promise { - let backup_string = this.mediaFileUri.readFileContent(myUri); - try { - this.decryptFile(backup_string, password).then((backup_string) => { - let backup: backup_file = JSON.parse(backup_string); - if (backup.magic == this.backup_magic) { - this.backupReload(backup.configs); - } else { - promptAction.showToast({ message: 'Error: invalid backup file!' }) - } - }) - } catch (err) { - promptAction.showToast({ message: err.message }) - } - - } - async callFilePickerSaveFile(): Promise { - this.getUIContext().showAlertDialog( { title: $r('app.string.setting_backup_output'), @@ -261,26 +126,20 @@ export struct SettingPage { { value: $r('app.string.setting_backup_type_default'), action: () => { - const documentSaveOptions = new picker.DocumentSaveOptions(); - documentSaveOptions.newFileNames = [`totp_backup_${generateFileNameWithDate()}.json`]; - documentSaveOptions.fileSuffixChoices = ['JSON|.json', '.json']; - let context = getContext(this) as common.Context; - const documentViewPicker = new picker.DocumentViewPicker(context); - documentViewPicker.save(documentSaveOptions).then((documentSaveResult: Array) => { - if (documentSaveResult !== null && documentSaveResult !== undefined && documentSaveResult.length == 1) { - this.backup_save_uri = documentSaveResult[0]; - Logger.info('documentViewPicker.save to file succeed and uris are:' + documentSaveResult); - this.writeContentForSaveAsFile(this.backup_save_uri); - } - }).catch((err: BusinessError) => { - Logger.error(`Invoke documentViewPicker.save failed, code is ${err.code}, message is ${err.message}`); - }) + showSaveFilePicker([`totp_backup_${generateFileNameWithDate()}.json`], ['JSON|.json']).then((uris) => { + const backup: TokenBackup = new TokenBackup(this.versionCode, this.arrConf); + saveBackupToFile(uris[0], backup).then(() => { + promptAction.showToast({ message: "Backup Success" }); + }).catch((reason: BusinessError) => { + promptAction.showToast({ message: reason.message }); + }); + }); } }, { value: $r('app.string.setting_backup_type_enc'), action: () => { - this.dialogController?.open(); + this.password_input_dialog?.open(); } }, { @@ -306,35 +165,26 @@ export struct SettingPage { } } ); - } async callFilePickerSelectFile(): Promise { - const documentSelectOptions = new picker.DocumentSelectOptions(); - documentSelectOptions.maxSelectNumber = 1; - documentSelectOptions.fileSuffixFilters = ['JSON|.json,BAK|.bak', '.json,.bak']; - let context = getContext(this) as common.Context; - const documentViewPicker = new picker.DocumentViewPicker(context); - documentViewPicker.select(documentSelectOptions).then((documentSelectResult: Array) => { - if (documentSelectResult !== null && documentSelectResult !== undefined && documentSelectResult.length == 1) { - this.backup_select_uri = documentSelectResult[0]; - Logger.info('documentViewPicker.select to file succeed and uris are:' + documentSelectResult); - if (this.backup_select_uri.endsWith('.bak')) { - //后缀bak为加密包 唤醒加密导入弹窗 - this.dialogController?.open(); - } else { - this.readContentFromSelectedFile(this.backup_select_uri); - } + showSelectFilePicker(1, ['JSON|.json', 'BAK|.bak']).then((uris) => { + this.backup_select_uri = uris[0]; + if (this.backup_select_uri.endsWith('.bak')) { + //后缀bak为加密包 唤醒加密导入弹窗 + this.password_input_dialog?.open(); + } else { + restoreFromBackup(uris[0]).then((backup) => { + this.backupReload(backup.configs); + }) } - }).catch((err: BusinessError) => { - Logger.error(`Invoke documentViewPicker.select failed, code is ${err.code}, message is ${err.message}`); - }) + }); } //endregion // 在自定义组件即将析构销毁时将dialogController置空 aboutToDisappear() { - this.dialogController = null // 将dialogController置空 + this.password_input_dialog = null // 将dialogController置空 this.appCtx.eventHub.off('DATASYNCPermissionsUpdate', this.updateDATASYNCPermissions); } diff --git a/entry/src/main/ets/utils/CryptoUtils.ets b/entry/src/main/ets/utils/CryptoUtils.ets new file mode 100644 index 0000000..1d13267 --- /dev/null +++ b/entry/src/main/ets/utils/CryptoUtils.ets @@ -0,0 +1,59 @@ +import { BusinessError } from '@kit.BasicServicesKit'; +import { cryptoFramework } from '@kit.CryptoArchitectureKit'; +import { buffer, util } from '@kit.ArkTS'; +import { stringToIntArray } from '../utils/TokenUtils' +import promptAction from '@ohos.promptAction' + +export async function decryptFile(file_buf: Uint8Array, user_key: string): Promise { + let aes_decoder = cryptoFramework.createCipher('AES128|CBC|PKCS7'); + let aes_iv: cryptoFramework.IvParamsSpec = { + algName: "IvParamsSpec", + iv: { data: stringToIntArray('ohtotptokenaesiv') } + }; + let spec: cryptoFramework.PBKDF2Spec = { + algName: 'PBKDF2', + password: user_key, + salt: new Uint8Array(16), + iterations: 10000, + keySize: 16 + }; + let kdf = cryptoFramework.createKdf('PBKDF2|SHA256'); + let symKeyBlob = kdf.generateSecretSync(spec); + let aesGenerator = cryptoFramework.createSymKeyGenerator('AES128'); + let aes_key = aesGenerator.convertKeySync(symKeyBlob); + await aes_decoder.init(cryptoFramework.CryptoMode.DECRYPT_MODE, aes_key, aes_iv) + .catch((reason: string) => { + promptAction.showToast({ message: reason }); + }); + return new Promise((resolve) => { + aes_decoder.doFinal({ data: file_buf }).then((decryptData) => { + let decoder = new util.TextDecoder() + resolve(decoder.decodeToString(decryptData.data)); + }).catch((reason: BusinessError) => { + promptAction.showToast({ message: reason.message }); + }); + }); +} + +export async function encryptFile(file_str: string, user_key: string): Promise { + let aes_encoder = cryptoFramework.createCipher('AES128|CBC|PKCS7'); + let aes_iv: cryptoFramework.IvParamsSpec = { + algName: "IvParamsSpec", + iv: { data: stringToIntArray('ohtotptokenaesiv') } + }; + let spec: cryptoFramework.PBKDF2Spec = { + algName: 'PBKDF2', + password: user_key, + salt: new Uint8Array(16), + iterations: 10000, + keySize: 16 + }; + let kdf = cryptoFramework.createKdf('PBKDF2|SHA256'); + let symKeyBlob = kdf.generateSecretSync(spec); + let aesGenerator = cryptoFramework.createSymKeyGenerator('AES128'); + let aes_key = aesGenerator.convertKeySync(symKeyBlob); + aes_encoder.initSync(cryptoFramework.CryptoMode.ENCRYPT_MODE, aes_key, aes_iv); + return aes_encoder.doFinal({ data: new Uint8Array(buffer.from(file_str).buffer) }).then((encryptData) => { + return encryptData.data; + }); +} diff --git a/entry/src/main/ets/utils/FileUtils.ets b/entry/src/main/ets/utils/FileUtils.ets new file mode 100644 index 0000000..2c91c40 --- /dev/null +++ b/entry/src/main/ets/utils/FileUtils.ets @@ -0,0 +1,47 @@ +import { fileIo, picker } from "@kit.CoreFileKit"; +import { common } from '@kit.AbilityKit'; +import { intArrayToString } from "./TokenUtils"; + +const BUFFER_SIZE = 4096; + +export async function readFileContent(uri: string): Promise { + let content = ''; + return fileIo.open(uri, fileIo.OpenMode.READ_ONLY).then((f) => { + let buffer = new ArrayBuffer(BUFFER_SIZE); + let offset = 0; + let readOut = 0; + do { + readOut = fileIo.readSync(f.fd, buffer, { offset: offset }); + content += intArrayToString(buffer.slice(0, readOut)); + offset += BUFFER_SIZE; + } while (readOut == BUFFER_SIZE) + fileIo.close(f.fd); + return content; + }); +} + +export async function writeFileContent(uri: string, content: string): Promise { + fileIo.open(uri, fileIo.OpenMode.READ_WRITE).then((f) => { + fileIo.write(f.fd, content).then(() => { + fileIo.close(f.fd); + }); + }); +} + +export async function showSelectFilePicker(max_num: number, filters: string[]): Promise { + const documentSelectOptions = new picker.DocumentSelectOptions(); + documentSelectOptions.maxSelectNumber = max_num; + documentSelectOptions.fileSuffixFilters = filters; + let context = AppStorage.get('appContext') as common.UIAbilityContext; + const documentViewPicker = new picker.DocumentViewPicker(context); + return documentViewPicker.select(documentSelectOptions); +} + +export async function showSaveFilePicker(name: string[], filters: string[]): Promise { + const documentSaveOptions = new picker.DocumentSaveOptions(); + documentSaveOptions.newFileNames = name; + documentSaveOptions.fileSuffixChoices = filters; + let context = AppStorage.get('appContext') as common.UIAbilityContext; + const documentViewPicker = new picker.DocumentViewPicker(context); + return documentViewPicker.save(documentSaveOptions); +} \ No newline at end of file diff --git a/entry/src/main/ets/utils/KvManager.ets b/entry/src/main/ets/utils/KvManager.ets index 29ad560..9dbf7ff 100644 --- a/entry/src/main/ets/utils/KvManager.ets +++ b/entry/src/main/ets/utils/KvManager.ets @@ -93,6 +93,7 @@ export class KvManager { } return null; } + //使用getEntries模拟一个可以兼容的数组 这样可以规避多设备覆盖的问题 async getStringArray(key: string): Promise { if (this._kvStore) { diff --git a/entry/src/main/ets/utils/MediaFileUri.ets b/entry/src/main/ets/utils/MediaFileUri.ets deleted file mode 100644 index 5066c4e..0000000 --- a/entry/src/main/ets/utils/MediaFileUri.ets +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright (c) 2024 Huawei Device Co., Ltd. - * Licensed under the Apache License, Version 2.0 (the 'License'); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an 'AS IS' BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { fileIo } from '@kit.CoreFileKit'; -import Logger from '../utils/Logger'; -import { intArrayToString } from '../utils/TokenUtils'; - -const TAG = 'MediaFileUri'; -const BUFFER_SIZE = 4096; // 文件读写缓冲区大小 -const COMMON_FD = -1; // 文件fd默认值 -const MODE_READ_ONLY = 0; -const MODE_WRITE_ONLY = 1; -const MODE_READ_WRITE = 2; - -export default class MediaFileUri { - private commonFd: number = COMMON_FD; - private fileSizeList: Array = []; - private fileNameList: Array = []; - private fileUriList: Array = []; - content: string = ''; - - getMode(openFlag: number): number { - let mode: number = 0; - switch (openFlag) { - case MODE_READ_ONLY: - mode = fileIo.OpenMode.READ_ONLY; // r - break; - case MODE_WRITE_ONLY: - mode = fileIo.OpenMode.WRITE_ONLY; // w - break; - case MODE_READ_WRITE: - mode = fileIo.OpenMode.READ_WRITE; // rw - break; - } - return mode; - } - - myWriteSync(fd: number, content: string, isClose: boolean): void { - try { - let result = fileIo.writeSync(fd, content); - Logger.info(TAG, 'myWriteSync: write result = ' + result); - } catch (err) { - Logger.error(TAG, 'myWriteSync: write failed with error:' + err); - } - if (isClose) { - this.closeSync(fd); - this.commonFd = COMMON_FD; - } else { - this.commonFd = fd; - } - } - - // sync-close - closeSync(fd: number): void { - try { - fileIo.closeSync(fd); - Logger.info(TAG, 'closeSync file finish.'); - } catch (err) { - Logger.error(TAG, 'closeSync file error = ' + err); - } - } - - readFileContent(uri: string, isRead: boolean = true, isClose: boolean = true): string { - let content = ''; - Logger.info(TAG, 'open path = ' + uri); - let file: fileIo.File; - if (isClose || this.commonFd === COMMON_FD) { - try { - file = fileIo.openSync(uri, fileIo.OpenMode.READ_ONLY); - Logger.info(TAG, 'openReadSync: get fd success. fd = ' + file.fd); - this.commonFd = file.fd; - } catch (err) { - Logger.error(TAG, 'openReadSync: open file failed. error = ' + err); - return content; - } - if (file === undefined) { - Logger.error(TAG, 'openReadSync: open file failed. file = undefined.'); - return content; - } - } - if (isRead) { - try { - let buffer = new ArrayBuffer(BUFFER_SIZE); - let offset = 0; - let readOut = 0; - do { - readOut = fileIo.readSync(this.commonFd, buffer, { offset: offset }); - content += intArrayToString(buffer.slice(0, readOut)); - offset += BUFFER_SIZE; - } while (readOut == BUFFER_SIZE) - } catch (err) { - Logger.error(TAG, 'myReadSync: read error: ' + err); - return content; - } - - if (isClose) { - this.closeSync(this.commonFd); - this.commonFd = COMMON_FD; - } else { - this.commonFd = this.commonFd; - } - } - return content; - } - - myGetFileSize(uri: string, mode: number): number { - let file = fileIo.openSync(uri, mode); // fs.OpenMode.READ_ONLY - Logger.info(TAG, 'file fd: ' + file.fd); - let stat = fileIo.statSync(file.fd); - Logger.info(TAG, 'get file info succeed, the size of file is ' + stat.size); - return stat.size; - } - - writeFileContent(uri: string, content: string): void { - Logger.info(TAG, 'writeFileContent begin'); - let file = fileIo.openSync(uri, fileIo.OpenMode.READ_WRITE); - Logger.info(TAG, 'writeFileContent file fd: ' + file.fd); - let writeLen = fileIo.writeSync(file.fd, content); - Logger.info(TAG, 'writeFileContent write data to file succeed and size is:' + writeLen); - fileIo.closeSync(file); - } - - async getAllFiles(): Promise { - Logger.info(TAG, 'getAllFiles begin'); - AppStorage.setOrCreate('fileNameList', this.fileNameList); - AppStorage.setOrCreate('fileSizeList', this.fileSizeList); - AppStorage.setOrCreate('fileUriList', this.fileUriList); - } -} \ No newline at end of file diff --git a/entry/src/main/ets/utils/TokenBackup.ets b/entry/src/main/ets/utils/TokenBackup.ets new file mode 100644 index 0000000..4f0d3a8 --- /dev/null +++ b/entry/src/main/ets/utils/TokenBackup.ets @@ -0,0 +1,41 @@ +import { readFileContent, writeFileContent } from "./FileUtils"; +import { TokenConfig } from "./TokenConfig"; +import { base32Decode, base32Encode, intArrayToString, stringToIntArray } from "./TokenUtils"; +import { decryptFile, encryptFile } from "./CryptoUtils"; +import { JSON } from "@kit.ArkTS"; +import { promptAction } from "@kit.ArkUI"; + +const TOKEN_BACKUP_MAGIC = 0x55aaeebb; + +export class TokenBackup { + magic: number; + version: number; + configs: Array; + + constructor(version: number, configs: Array) { + this.magic = TOKEN_BACKUP_MAGIC; + this.version = version; + this.configs = configs; + } +} + +export async function saveBackupToFile(file_uri: string, backup: TokenBackup, do_encrypt: boolean = false, password: string = ''): Promise { + const backup_json = JSON.stringify(backup); + const backup_raw = do_encrypt ? await encryptFile(backup_json, password) : stringToIntArray(backup_json); + const b32_encoded = base32Encode(backup_raw); + writeFileContent(file_uri, b32_encoded); +} + +export async function restoreFromBackup(file_uri: string, is_encrypt: boolean = false, password: string = ''): Promise { + const file_content = await readFileContent(file_uri); + const b32_decoded = base32Decode(file_content); + const backup_string = is_encrypt ? await decryptFile(b32_decoded, password) : intArrayToString(b32_decoded.buffer as ArrayBuffer); + const backup: TokenBackup = JSON.parse(backup_string) as TokenBackup; + return new Promise((resolve, reject) => { + if (backup.magic === TOKEN_BACKUP_MAGIC) { + resolve(backup) + } else { + reject('invalid backup file') + } + }) +}