1/* 2 * Copyright (c) 2025 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16import AccountAssociation from '../bean/data/AccountAssociation'; 17import AccountAssociationUtil from '../common/AssocUtil/AccountAssociationUtil'; 18import AppStorageMgr from '../common/AppStorageMgr'; 19import AuthorizedAccount from '../bean/data/AuthorizedAccount'; 20import Constants from '../common/constant'; 21import { getCurrentTime, isInvalidStr } from '../common/FileUtils/utils'; 22import { HiLog } from '../common/HiLog'; 23import HuksCipherUtils from '../common/huks/HuksCipherUtil'; 24import StorageUtil from '../common/StorageUtil'; 25import AssociationDecryptValidator from '../validator/AccountAssociation/AssociationDecryptValidator'; 26import AssociationEncryptValidator from '../validator/AccountAssociation/AssociationEncryptValidator'; 27 28const TAG: string = 'AssociationMgr'; 29 30export default class AccountAssociationManager { 31 private static mInstance: AccountAssociationManager | null = null; 32 private storageUtil: StorageUtil | null = null; 33 34 private constructor() { 35 } 36 37 public static getInstance(): AccountAssociationManager | null { 38 const appContext = AppStorageMgr.getApplicationContext(); 39 if (!appContext) { 40 HiLog.error(TAG, 'Get Application Context failed.'); 41 return null; 42 } 43 if (!AccountAssociationManager.mInstance) { 44 AccountAssociationManager.mInstance = new AccountAssociationManager(); 45 AccountAssociationManager.mInstance.storageUtil = 46 StorageUtil.getInstance(appContext); 47 } 48 return AccountAssociationManager.mInstance; 49 } 50 51 private async getAssociationFromLocal(ownerAccountId: string): Promise<AccountAssociation | null> { 52 HiLog.info(TAG, 'Start getting account association data from local.'); 53 if (isInvalidStr(ownerAccountId)) { 54 HiLog.error(TAG, 'Owner account ID is invalid'); 55 return null; 56 } 57 58 const validator = AssociationDecryptValidator.getInstance(); 59 let res: AccountAssociation | null = await validator.validate(ownerAccountId); 60 if (res) { 61 HiLog.info(TAG, 'Get account association data from local success.'); 62 return res; 63 } 64 HiLog.error(TAG, 'Get account association data from local failed.'); 65 return null; 66 } 67 68 public async getAuthorizedAccount(ownerAccountId: string): Promise<AuthorizedAccount[]> { 69 HiLog.info(TAG, 'Enter getAuthorizedAccount.'); 70 71 if (isInvalidStr(ownerAccountId)) { 72 HiLog.error(TAG, `The ownerAccountId is invalid. Code is ${Constants.ERR_CODE_ASSOCIATION_GET_FAILED}`); 73 return []; 74 } 75 let accountAssociation = await this.getAssociationFromLocal(ownerAccountId); 76 if (!accountAssociation) { 77 HiLog.info(TAG, 'No account association data exists in this device. Create a new one.'); 78 accountAssociation = new AccountAssociation(ownerAccountId, []); 79 } else { 80 HiLog.info(TAG, 'The account association data has existed in this device. Decrypt and read it directly.'); 81 } 82 return accountAssociation.getAuthorizedAccounts(); 83 } 84 85 private async updateAuthorizedAccount( 86 ownerAccountId: string, 87 authorizedAccount: string 88 ): Promise<AuthorizedAccount[] | null> { 89 HiLog.info(TAG, 'Enter updateAuthorizedAccount.'); 90 91 let currentTime: number = getCurrentTime(false); 92 let authorizedAccounts: AuthorizedAccount[] = await this.getAuthorizedAccount(ownerAccountId); 93 let accountData: AuthorizedAccount[] = 94 authorizedAccounts.filter(account => account.getUserAccount() === authorizedAccount); 95 96 if (accountData.length === 0) { 97 HiLog.info(TAG, 'No authorized account infomation. Create a new one.'); 98 if (authorizedAccounts.length === Constants.ASSOCIATION_MAX_SIZE) { 99 HiLog.info(TAG, 100 `The authorized account list has reached the limit of ${Constants.ASSOCIATION_MAX_SIZE}.`); 101 HiLog.info(TAG, 'Update the authorized account list.'); 102 103 let accountAssociation = new AccountAssociation(ownerAccountId, authorizedAccounts); 104 if (!accountAssociation) { 105 HiLog.error(TAG, 'Generate account association object failed.'); 106 return null; 107 } 108 109 const earliestAccount = accountAssociation.removeEarliestAuthorizedAccount(); 110 if (!earliestAccount) { 111 HiLog.error(TAG, 'Remove the earliest authorized account failed.'); 112 return null; 113 } 114 authorizedAccounts = accountAssociation.getAuthorizedAccounts(); 115 } 116 authorizedAccounts.push(new AuthorizedAccount(authorizedAccount, currentTime)); 117 } else { 118 HiLog.info(TAG, 'The authorized account info already exists. Refresh the timestamp.'); 119 authorizedAccounts.forEach(account => { 120 if (account.getUserAccount() === authorizedAccount) { 121 account.setTimestamp(currentTime); 122 } 123 }); 124 } 125 return authorizedAccounts; 126 } 127 128 private async encryptAndSaveAssociation( 129 storageUtil: StorageUtil | null, 130 association: AccountAssociation, 131 saveFileName: string 132 ): Promise<boolean> { 133 HiLog.info(TAG, 'Start encrypting and saving account association.'); 134 135 if (!storageUtil) { 136 HiLog.error(TAG, 'Storage util is null.'); 137 return false; 138 } 139 140 const validator = AssociationEncryptValidator.getInstance(); 141 let res: boolean = await validator.validate(storageUtil, association, saveFileName); 142 if (!res) { 143 HiLog.error(TAG, 'EncryptAndSaveAssociation failed.'); 144 return false; 145 } 146 HiLog.info(TAG, 'Encrypt and save account association data success.'); 147 return true; 148 } 149 150 public async setAuthorizedAccount(ownerAccountId: string, authorizedAccount: string): Promise<void> { 151 HiLog.info(TAG, 'Enter setAuthorizedAccount.'); 152 153 if (isInvalidStr(ownerAccountId) || isInvalidStr(authorizedAccount)) { 154 HiLog.error(TAG, 155 `Input owerAccountId or authorizedAccount is invalid. Code is ${Constants.ERR_CODE_ASSOCIATION_SET_FAILED}`); 156 return; 157 } 158 159 try { 160 let authorizedAccounts = await this.updateAuthorizedAccount(ownerAccountId, authorizedAccount); 161 if (!authorizedAccounts) { 162 HiLog.error(TAG, `Update authorized account list failed. Code is ${Constants.ERR_CODE_ASSOCIATION_SET_FAILED}`); 163 return; 164 } 165 166 let updatedAssociation = new AccountAssociation(ownerAccountId, authorizedAccounts); 167 let associationFileName = await AccountAssociationUtil.getAccountAssociationFileName(ownerAccountId); 168 if (!associationFileName) { 169 HiLog.error(TAG, 'Get association file name failed.'); 170 return; 171 } 172 173 let saveResult = await this.encryptAndSaveAssociation(this.storageUtil, updatedAssociation, associationFileName); 174 if (!saveResult) { 175 HiLog.error(TAG, 'Storage authorized account failed.'); 176 } 177 } catch (error) { 178 HiLog.error(TAG, `Set authorized account error: ${JSON.stringify(error)}`); 179 } 180 } 181 182 public async deleteAuthorizedAccount(ownerAccountId: string): Promise<void> { 183 HiLog.info(TAG, 'Enter deleteAuthorizedAccount.'); 184 185 if (isInvalidStr(ownerAccountId)) { 186 HiLog.error(TAG, `Input ownerAccountId is invalid. Code is ${Constants.ERR_CODE_ASSOCIATION_DELETE_FAILED}`); 187 return; 188 } 189 await HuksCipherUtils.deleteKey(Constants.ASSOCIATION_KEY_ALIAS); 190 191 let associationFileName = await AccountAssociationUtil.getAccountAssociationFileName(ownerAccountId); 192 if (!associationFileName) { 193 HiLog.error(TAG, 'Get association file name failed.'); 194 return; 195 } 196 if (!this.storageUtil) { 197 HiLog.error(TAG, 'Get storage util failed.'); 198 return; 199 } 200 201 await this.storageUtil.deleteData(Constants.ASSOCIATION_STORAGE_KEY, associationFileName); 202 await this.storageUtil.deleteData(Constants.AES_NONCE_KEY, associationFileName); 203 await this.storageUtil.deleteFile(associationFileName); 204 } 205}