diff --git a/controllers/VaultController.php b/controllers/VaultController.php index ba69fbc..807992b 100644 --- a/controllers/VaultController.php +++ b/controllers/VaultController.php @@ -33,7 +33,7 @@ class VaultController extends Controller 'rules' => [ [ 'allow' => true, - 'actions' => ['index', 'download', 'delete', 'upload', 'init', 'auth'], + 'actions' => ['index', 'download', 'delete', 'upload', 'init', 'auth', 'get-salt'], 'roles' => ['user'], ], ], @@ -47,6 +47,7 @@ class VaultController extends Controller 'upload' => ['POST'], 'init' => ['POST'], 'auth' => ['POST'], + 'get-salt' => ['GET'], ], ], ] @@ -263,6 +264,7 @@ class VaultController extends Controller $model = Yii::$app->user->identity; // 获取当前用户模型 if ($model->load(Yii::$app->request->post()) && $model->validate() && !empty($model->input_vault_secret)) { $model->vault_secret = Yii::$app->getSecurity()->generatePasswordHash($model->input_vault_secret); + $model->vault_salt = Yii::$app->getSecurity()->generateRandomString(64); if ($model->save(false)) { // 保存用户模型 Yii::$app->session->setFlash('success', '保险箱初始化成功,请牢记密码,否则无法恢复保险箱内文件'); } else { @@ -293,4 +295,16 @@ class VaultController extends Controller } return $this->redirect('index.php?r=vault%2Findex'); } + + /** + * 获取保险箱密码盐 + * GET + * @return array + */ + public function actionGetSalt(): array + { + Yii::$app->response->format = Response::FORMAT_JSON; + $user = Yii::$app->user->identity; + return ['vault_salt' => $user->vault_salt]; + } } \ No newline at end of file diff --git a/models/User.php b/models/User.php index 6cc54b4..2f9ebc6 100644 --- a/models/User.php +++ b/models/User.php @@ -30,6 +30,7 @@ use yii\web\IdentityInterface; * @property string|null $recovery_codes OTP恢复代码 * @property int|null $dark_mode 夜间模式(0 off,1 on,2 auto) * @property string|null $vault_secret 保险箱密钥 + * @property string|null $vault_salt 保险箱加密密钥盐 * * @property CollectionTasks[] $collectionTasks * @property Share[] $shares @@ -63,7 +64,7 @@ class User extends ActiveRecord implements IdentityInterface return [ [['status', 'is_encryption_enabled', 'is_otp_enabled', 'dark_mode'], 'integer'], [['created_at', 'last_login'], 'safe'], - [['bio', 'totp_input', 'recoveryCode_input', 'name'], 'string'], + [['bio', 'totp_input', 'recoveryCode_input', 'name','vault_salt'], 'string'], ['input_vault_secret', 'string', 'min' => 6, 'max' => 24], [['encryption_key', 'otp_secret', 'recovery_codes', 'vault_secret'], 'string', 'max' => 255], [['last_login_ip'], 'string', 'max' => 45], @@ -122,7 +123,8 @@ class User extends ActiveRecord implements IdentityInterface 'storage_limit' => 'Storage Limit', 'recovery_codes' => 'Recovery Codes', 'dark_mode' => 'Dark Mode', - 'vault_secret' => 'Vault Secret' + 'vault_secret' => 'Vault Secret', + 'vault_salt' => 'Vault Salt', ]; } diff --git a/views/vault/_gateway.php b/views/vault/_gateway.php index 3da4770..06ab5a0 100644 --- a/views/vault/_gateway.php +++ b/views/vault/_gateway.php @@ -19,7 +19,7 @@ $this->params['breadcrumbs'][] = $this->title;
'gateway-vault-form', 'action' => ['vault/auth'], 'method' => 'post']); ?> - field($model, 'vault_secret')->passwordInput()->label('保险箱密码(不是登陆密码)') ?> + field($model, 'vault_secret')->passwordInput(['id'=>'password'])->label('保险箱密码(不是登陆密码)') ?>
'btn btn-primary']) ?>
diff --git a/web/js/vault_core.js b/web/js/vault_core.js index f5ecc56..29f18b1 100644 --- a/web/js/vault_core.js +++ b/web/js/vault_core.js @@ -1,6 +1,15 @@ const vaultRawKey = sessionStorage.getItem('vaultRawKey'); -async function generateEncryptionKeyFromPassword(password) { + +async function getSaltFromBackend() { + const response = await fetch('index.php?r=vault%2Fget-salt'); + const data = await response.json(); + return data.vault_salt; +} + +async function deriveKey(password) { const passwordBuffer = new TextEncoder().encode(password); + const salt = await getSaltFromBackend(); + const saltBuffer = new TextEncoder().encode(salt); const key = await window.crypto.subtle.importKey( 'raw', passwordBuffer, @@ -11,102 +20,58 @@ async function generateEncryptionKeyFromPassword(password) { return await window.crypto.subtle.deriveKey( { name: 'PBKDF2', - salt: new Uint8Array([]), + salt: saltBuffer, iterations: 100000, hash: 'SHA-256' }, key, {name: 'AES-GCM', length: 256}, - false, + false, // 是否允许导出 ['encrypt', 'decrypt'] ); } -// 加密文件 + async function encryptFile(file, password) { - const iv = window.crypto.getRandomValues(new Uint8Array(12)); // 生成随机 IV - const passwordBuffer = new TextEncoder().encode(password); - const key = await window.crypto.subtle.importKey( - 'raw', - passwordBuffer, - {name: 'PBKDF2'}, - false, - ['deriveKey'] + const iv = window.crypto.getRandomValues(new Uint8Array(12)); + const derivedKey = await deriveKey(password); + // console.log(password); + const plaintextData = await file.arrayBuffer(); + const encryptedData = await window.crypto.subtle.encrypt( + {name: 'AES-GCM', iv: iv, tagLength: 128}, + derivedKey, + plaintextData ); - const derivedKey = await window.crypto.subtle.deriveKey( - { - name: 'PBKDF2', - salt: new Uint8Array([]), - iterations: 100000, - hash: 'SHA-256' - }, - key, - {name: 'AES-GCM', length: 256}, - false, - ['encrypt', 'decrypt'] - ); - - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = function(event) { - const plaintextData = event.target.result; - window.crypto.subtle.encrypt( - { name: 'AES-GCM', iv: iv }, // 使用随机生成的 IV - derivedKey, - plaintextData - ).then(encryptedData => { - const encryptedBlob = new Blob([iv, encryptedData], {type: file.type}); - resolve(encryptedBlob); - }).catch(error => { - reject(error); - }); - }; - reader.readAsArrayBuffer(file); - }); + return new Blob([iv, encryptedData], {type: file.type}); } -// 解密文件 async function decryptFile(encryptedFile, password) { - return new Promise((resolve, reject) => { - const fileReader = new FileReader(); - fileReader.onload = async function(event) { - try { - const encryptedData = new Uint8Array(event.target.result); - const iv = encryptedData.slice(0, 12); // 从密文中提取 IV - const ciphertext = encryptedData.slice(12); - const passwordBuffer = new TextEncoder().encode(password); - const key = await window.crypto.subtle.importKey( - 'raw', - passwordBuffer, - {name: 'PBKDF2'}, - false, - ['deriveKey'] - ); - const derivedKey = await window.crypto.subtle.deriveKey( - { - name: 'PBKDF2', - salt: new Uint8Array([]), - iterations: 100000, - hash: 'SHA-256' - }, - key, - {name: 'AES-GCM', length: 256}, - false, - ['encrypt', 'decrypt'] - ); - const decryptedData = await window.crypto.subtle.decrypt( - { name: 'AES-GCM', iv: iv }, // 使用从密文中提取的 IV - derivedKey, - ciphertext - ); - const decryptedFile = new Blob([decryptedData], { type: encryptedFile.type }); - resolve(decryptedFile); - } catch (error) { - reject(error); - } - }; - fileReader.readAsArrayBuffer(encryptedFile); - }); + const encryptedData = new Uint8Array(await encryptedFile.arrayBuffer()); + const iv = encryptedData.slice(0, 12); + const ciphertext = encryptedData.slice(12); + const derivedKey = await deriveKey(password); + const decryptedData = await window.crypto.subtle.decrypt( + {name: 'AES-GCM', iv: iv, tagLength: 128}, + derivedKey, + ciphertext + ); + return new Blob([decryptedData], {type: encryptedFile.type}); } +// async function decryptFile(encryptedFile, password) { +// const encryptedData = new Uint8Array(await encryptedFile.arrayBuffer()); +// const iv = encryptedData.slice(0, 12); +// const ciphertext = encryptedData.slice(12); +// const derivedKey = await deriveKey(password); +// const keyData = await window.crypto.subtle.exportKey('raw', derivedKey); +// const keyBytes = new Uint8Array(keyData); +// // console.log('Key:', keyBytes); +// // console.log(password); +// const decryptedData = await window.crypto.subtle.decrypt( +// {name: 'AES-GCM', iv: iv, tagLength: 128}, +// derivedKey, +// ciphertext +// ); +// return new Blob([decryptedData], {type: encryptedFile.type}); +// } async function downloadAndDecryptFile(url, password, filename) { const response = await fetch(url); const encryptedFile = await response.blob();