加入随机的salt提高安全性

修复无法使用password作为加密密钥生成所需的Bug
*准备了一个windows程序来加解密(https://git.chenx221.cyou/chenx221/vault-decryptFile)
This commit is contained in:
Chenx221 2024-03-12 17:04:19 +08:00
parent f83c7ed325
commit 5878155901
Signed by: chenx221
GPG Key ID: D7A9EC07024C3021
4 changed files with 68 additions and 87 deletions

View File

@ -33,7 +33,7 @@ class VaultController extends Controller
'rules' => [ 'rules' => [
[ [
'allow' => true, 'allow' => true,
'actions' => ['index', 'download', 'delete', 'upload', 'init', 'auth'], 'actions' => ['index', 'download', 'delete', 'upload', 'init', 'auth', 'get-salt'],
'roles' => ['user'], 'roles' => ['user'],
], ],
], ],
@ -47,6 +47,7 @@ class VaultController extends Controller
'upload' => ['POST'], 'upload' => ['POST'],
'init' => ['POST'], 'init' => ['POST'],
'auth' => ['POST'], 'auth' => ['POST'],
'get-salt' => ['GET'],
], ],
], ],
] ]
@ -263,6 +264,7 @@ class VaultController extends Controller
$model = Yii::$app->user->identity; // 获取当前用户模型 $model = Yii::$app->user->identity; // 获取当前用户模型
if ($model->load(Yii::$app->request->post()) && $model->validate() && !empty($model->input_vault_secret)) { 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_secret = Yii::$app->getSecurity()->generatePasswordHash($model->input_vault_secret);
$model->vault_salt = Yii::$app->getSecurity()->generateRandomString(64);
if ($model->save(false)) { // 保存用户模型 if ($model->save(false)) { // 保存用户模型
Yii::$app->session->setFlash('success', '保险箱初始化成功,请牢记密码,否则无法恢复保险箱内文件'); Yii::$app->session->setFlash('success', '保险箱初始化成功,请牢记密码,否则无法恢复保险箱内文件');
} else { } else {
@ -293,4 +295,16 @@ class VaultController extends Controller
} }
return $this->redirect('index.php?r=vault%2Findex'); 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];
}
} }

View File

@ -30,6 +30,7 @@ use yii\web\IdentityInterface;
* @property string|null $recovery_codes OTP恢复代码 * @property string|null $recovery_codes OTP恢复代码
* @property int|null $dark_mode 夜间模式(0 off,1 on,2 auto) * @property int|null $dark_mode 夜间模式(0 off,1 on,2 auto)
* @property string|null $vault_secret 保险箱密钥 * @property string|null $vault_secret 保险箱密钥
* @property string|null $vault_salt 保险箱加密密钥盐
* *
* @property CollectionTasks[] $collectionTasks * @property CollectionTasks[] $collectionTasks
* @property Share[] $shares * @property Share[] $shares
@ -63,7 +64,7 @@ class User extends ActiveRecord implements IdentityInterface
return [ return [
[['status', 'is_encryption_enabled', 'is_otp_enabled', 'dark_mode'], 'integer'], [['status', 'is_encryption_enabled', 'is_otp_enabled', 'dark_mode'], 'integer'],
[['created_at', 'last_login'], 'safe'], [['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], ['input_vault_secret', 'string', 'min' => 6, 'max' => 24],
[['encryption_key', 'otp_secret', 'recovery_codes', 'vault_secret'], 'string', 'max' => 255], [['encryption_key', 'otp_secret', 'recovery_codes', 'vault_secret'], 'string', 'max' => 255],
[['last_login_ip'], 'string', 'max' => 45], [['last_login_ip'], 'string', 'max' => 45],
@ -122,7 +123,8 @@ class User extends ActiveRecord implements IdentityInterface
'storage_limit' => 'Storage Limit', 'storage_limit' => 'Storage Limit',
'recovery_codes' => 'Recovery Codes', 'recovery_codes' => 'Recovery Codes',
'dark_mode' => 'Dark Mode', 'dark_mode' => 'Dark Mode',
'vault_secret' => 'Vault Secret' 'vault_secret' => 'Vault Secret',
'vault_salt' => 'Vault Salt',
]; ];
} }

View File

@ -19,7 +19,7 @@ $this->params['breadcrumbs'][] = $this->title;
<div class="row"> <div class="row">
<div class="col-lg-5"> <div class="col-lg-5">
<?php $form = ActiveForm::begin(['id' => 'gateway-vault-form', 'action' => ['vault/auth'], 'method' => 'post']); ?> <?php $form = ActiveForm::begin(['id' => 'gateway-vault-form', 'action' => ['vault/auth'], 'method' => 'post']); ?>
<?= $form->field($model, 'vault_secret')->passwordInput()->label('保险箱密码(不是登陆密码)') ?> <?= $form->field($model, 'vault_secret')->passwordInput(['id'=>'password'])->label('保险箱密码(不是登陆密码)') ?>
<div class="form-group"> <div class="form-group">
<?= Html::submitButton('确认', ['class' => 'btn btn-primary']) ?> <?= Html::submitButton('确认', ['class' => 'btn btn-primary']) ?>
</div> </div>

View File

@ -1,6 +1,15 @@
const vaultRawKey = sessionStorage.getItem('vaultRawKey'); 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 passwordBuffer = new TextEncoder().encode(password);
const salt = await getSaltFromBackend();
const saltBuffer = new TextEncoder().encode(salt);
const key = await window.crypto.subtle.importKey( const key = await window.crypto.subtle.importKey(
'raw', 'raw',
passwordBuffer, passwordBuffer,
@ -11,102 +20,58 @@ async function generateEncryptionKeyFromPassword(password) {
return await window.crypto.subtle.deriveKey( return await window.crypto.subtle.deriveKey(
{ {
name: 'PBKDF2', name: 'PBKDF2',
salt: new Uint8Array([]), salt: saltBuffer,
iterations: 100000, iterations: 100000,
hash: 'SHA-256' hash: 'SHA-256'
}, },
key, key,
{name: 'AES-GCM', length: 256}, {name: 'AES-GCM', length: 256},
false, false, // 是否允许导出
['encrypt', 'decrypt'] ['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 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) => { async function encryptFile(file, password) {
const reader = new FileReader(); const iv = window.crypto.getRandomValues(new Uint8Array(12));
reader.onload = function(event) { const derivedKey = await deriveKey(password);
const plaintextData = event.target.result; // console.log(password);
window.crypto.subtle.encrypt( const plaintextData = await file.arrayBuffer();
{ name: 'AES-GCM', iv: iv }, // 使用随机生成的 IV const encryptedData = await window.crypto.subtle.encrypt(
{name: 'AES-GCM', iv: iv, tagLength: 128},
derivedKey, derivedKey,
plaintextData plaintextData
).then(encryptedData => { );
const encryptedBlob = new Blob([iv, encryptedData], {type: file.type}); return new Blob([iv, encryptedData], {type: file.type});
resolve(encryptedBlob);
}).catch(error => {
reject(error);
});
};
reader.readAsArrayBuffer(file);
});
} }
// 解密文件
async function decryptFile(encryptedFile, password) { async function decryptFile(encryptedFile, password) {
return new Promise((resolve, reject) => { const encryptedData = new Uint8Array(await encryptedFile.arrayBuffer());
const fileReader = new FileReader(); const iv = encryptedData.slice(0, 12);
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 ciphertext = encryptedData.slice(12);
const passwordBuffer = new TextEncoder().encode(password); const derivedKey = await deriveKey(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( const decryptedData = await window.crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: iv }, // 使用从密文中提取的 IV {name: 'AES-GCM', iv: iv, tagLength: 128},
derivedKey, derivedKey,
ciphertext ciphertext
); );
const decryptedFile = new Blob([decryptedData], { type: encryptedFile.type }); return new Blob([decryptedData], {type: encryptedFile.type});
resolve(decryptedFile);
} catch (error) {
reject(error);
}
};
fileReader.readAsArrayBuffer(encryptedFile);
});
} }
// 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) { async function downloadAndDecryptFile(url, password, filename) {
const response = await fetch(url); const response = await fetch(url);
const encryptedFile = await response.blob(); const encryptedFile = await response.blob();