Java 密钥加密外包

Java 密钥加密外包,java,security,passwords,password-protection,password-encryption,Java,Security,Passwords,Password Protection,Password Encryption,我正在创建一个密码管理器,它使用SQLite数据库来存储用户密码。我用AES算法加密密码,但我不知道从主密码生成的加密密钥应该保存在哪里。重要信息:聘请专家。认真地这东西很硬。即使是专家也弄错了。但他们知道该注意什么 话虽如此: 所以这是一个非常常见的模式。有几种方法,但它们都有一些共同的模式 密钥派生 密钥派生步骤发生在获取密钥并从中派生密钥时。此过程使用了一个 在本例中,您需要一个PBKDF,它是一个基于密码的密钥派生函数。这是一个类似于KDF的函数,但设计用于低熵机密。因此,它增加了一层保

我正在创建一个密码管理器,它使用SQLite数据库来存储用户密码。我用AES算法加密密码,但我不知道从主密码生成的加密密钥应该保存在哪里。

重要信息:聘请专家。认真地这东西很硬。即使是专家也弄错了。但他们知道该注意什么

话虽如此:

所以这是一个非常常见的模式。有几种方法,但它们都有一些共同的模式

密钥派生 密钥派生步骤发生在获取密钥并从中派生密钥时。此过程使用了一个

在本例中,您需要一个PBKDF,它是一个基于密码的密钥派生函数。这是一个类似于KDF的函数,但设计用于低熵机密。因此,它增加了一层保护,使得从秘密到密钥的链接更难找到(从而更难尝试暴力强迫)

一个常用的PBKDF函数是。您也可以使用(更新,但更强)。从这里开始,我将使用pbkdf2,但肯定也会研究scrypt(它有更多的调优参数,但在使用方面是相同的)

首先,你需要生成一种盐。这是一个随机值。这允许相同的密码在不同的系统上生成不同的密钥。盐不是秘密

salt := genRandom(16)
这种盐需要存储在某个地方(可能在数据库中,或者类似的地方)

然后,从密码中派生一个密钥。请注意,对于具有相同设置(和salt)的每个派生,相同的密码将产生相同的密钥。因此,我们不存储此密钥。每次我们需要它时都会计算出来

key := pbkdf2('sha256', password, salt, count, keyLength)
请注意count参数。这提供了防止暴力强迫的保护。你做的越高,函数所用的时间就越长。对于在线使用(例如在web请求中),10000-20000的值是合适的。对于离线使用(就像你的一样),你可以容忍更高(而且越高越好)

现在我们从密码中导出了一个密钥

加密 我们可以直接用那个密钥加密。因此,这意味着当用户打开应用程序时,他们将输入密码,密码将导出密钥并存储在内存中

这是一把双刃剑。很简单。但这也意味着,如果任何人都可以从内存中读取密钥,那么他们就可以开始强行输入密码

另一种选择是存储主密钥。这将在安装时生成,并使用派生密钥进行加密。因此,当用户登录时,您将派生一个密钥,检索主密钥,然后擦除派生密钥。这提供了一些针对暴力强迫的保护,并允许加密使用更强大的密钥。它还提供了更改密码的能力,而无需重新加密所有内容

因此,流看起来像:

private masterKey;
function login(password) {
    salt = lookupSalt()
    derivedKey = pbkdf2('sha256', password, salt, count, keyLength)
    masterKeyEncrypted = lookupEncryptedMasterKey()
    // NOTE that you **must** authenticate this encryption
    masterKey = decrypt(masterKeyEncrypted, derivedKey)
    derivedKey = null 
}
然后,只需使用主密钥进行加密和解密

认证 所以我提到您必须对加密进行验证。基本上,这意味着使用

伪码

function encrypt(data, key) {
    cipherKey = key[0...256] // first 256 bits
    macKey = key[256...512] // second 256 bits
    iv = key[512...640] // final 128 bits
    cipherText = AES-256-CBC-ENCRYPT(data, cipherKey, iv)
    mac = HMAC('sha256', cipherText | iv, macKey)
    return mac | cipherText
}
然后,在解密时,您将执行相同的操作:

function decrypt(data, key) {
    cipherKey = key[0...256] // first 256 bits
    macKey = key[256...512] // second 256 bits
    iv = key[512...640] // final 128 bits
    mac = data[0...256] // first 256 bits
    cipherText = data[256...] // rest
    if ( mac != HMAC('sha256', cipherText | iv, macKey)) 
        throw Exception "Invalid Data"
    return AES-256-CBC-DECRYPT(cipherText, cipherKey, iv)
}
现在,MAC检查应该通过定时安全比较来完成(以防止侧通道定时攻击)

请注意,由于身份验证取决于密码,因此无效密码将导致身份验证失败。但还要注意的是,您无法区分无效密码和有人篡改存储的密钥之间的区别


如果这是一个重大问题,那么您可以再次散列密码(使用不同的salt),并将其加密存储在数据库中。然后,在解密主密钥之后,解密存储的密码散列并验证密码是否满足散列。如果您这样做,请务必对散列进行加密,并使用不同的salt(否则会丢掉其余的工作)。

请认真阅读:我的应用程序是一个密码管理器。如果我理解正确,我必须散列主密码并加密SQLite表的密码?因为散列无法恢复原始文本。@AlexanderTobiasHeinrich该链接太棒了。不应该进行审核以确保输入的密码正确吗?