加入随机的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' => [
|
||||
[
|
||||
'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];
|
||||
}
|
||||
}
|
@ -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',
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,7 @@ $this->params['breadcrumbs'][] = $this->title;
|
||||
<div class="row">
|
||||
<div class="col-lg-5">
|
||||
<?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">
|
||||
<?= Html::submitButton('确认', ['class' => 'btn btn-primary']) ?>
|
||||
</div>
|
||||
|
@ -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 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
|
||||
async function encryptFile(file, password) {
|
||||
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
|
||||
).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 encryptedData = new Uint8Array(await encryptedFile.arrayBuffer());
|
||||
const iv = encryptedData.slice(0, 12);
|
||||
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 derivedKey = await deriveKey(password);
|
||||
const decryptedData = await window.crypto.subtle.decrypt(
|
||||
{ name: 'AES-GCM', iv: iv }, // 使用从密文中提取的 IV
|
||||
{name: 'AES-GCM', iv: iv, tagLength: 128},
|
||||
derivedKey,
|
||||
ciphertext
|
||||
);
|
||||
const decryptedFile = new Blob([decryptedData], { type: encryptedFile.type });
|
||||
resolve(decryptedFile);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
fileReader.readAsArrayBuffer(encryptedFile);
|
||||
});
|
||||
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();
|
||||
|
Loading…
Reference in New Issue
Block a user