加入随机的salt提高安全性
修复无法使用password作为加密密钥生成所需的Bug *准备了一个windows程序来加解密(https://git.chenx221.cyou/chenx221/vault-decryptFile)
This commit is contained in:
parent
f83c7ed325
commit
5878155901
@ -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];
|
||||||
|
}
|
||||||
}
|
}
|
@ -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',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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) {
|
async function encryptFile(file, password) {
|
||||||
const iv = window.crypto.getRandomValues(new Uint8Array(12)); // 生成随机 IV
|
const iv = window.crypto.getRandomValues(new Uint8Array(12));
|
||||||
const passwordBuffer = new TextEncoder().encode(password);
|
const derivedKey = await deriveKey(password);
|
||||||
const key = await window.crypto.subtle.importKey(
|
// console.log(password);
|
||||||
'raw',
|
const plaintextData = await file.arrayBuffer();
|
||||||
passwordBuffer,
|
const encryptedData = await window.crypto.subtle.encrypt(
|
||||||
{name: 'PBKDF2'},
|
{name: 'AES-GCM', iv: iv, tagLength: 128},
|
||||||
false,
|
derivedKey,
|
||||||
['deriveKey']
|
plaintextData
|
||||||
);
|
);
|
||||||
const derivedKey = await window.crypto.subtle.deriveKey(
|
return new Blob([iv, encryptedData], {type: file.type});
|
||||||
{
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解密文件
|
|
||||||
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) {
|
const ciphertext = encryptedData.slice(12);
|
||||||
try {
|
const derivedKey = await deriveKey(password);
|
||||||
const encryptedData = new Uint8Array(event.target.result);
|
const decryptedData = await window.crypto.subtle.decrypt(
|
||||||
const iv = encryptedData.slice(0, 12); // 从密文中提取 IV
|
{name: 'AES-GCM', iv: iv, tagLength: 128},
|
||||||
const ciphertext = encryptedData.slice(12);
|
derivedKey,
|
||||||
const passwordBuffer = new TextEncoder().encode(password);
|
ciphertext
|
||||||
const key = await window.crypto.subtle.importKey(
|
);
|
||||||
'raw',
|
return new Blob([decryptedData], {type: encryptedFile.type});
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
// 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();
|
||||||
|
Loading…
Reference in New Issue
Block a user