一種保護云中的密碼的加密方法
簡介
在本文中,后端開發(fā)人員可以了解為什么使用加密很重要,以及如何有效地使用它來保護云上的用戶信息(特別是密碼),使得數(shù)據(jù)即使泄露也不會在數(shù)十年內被破解。安全性是云中的一個非常重要的主題,它對全棧開發(fā)至關重要,而且在所有產(chǎn)品和服務上都不可或缺。
我們首先會列出一些在開發(fā)中考慮安全性時要執(zhí)行(或不執(zhí)行)的簡單事務:
- 始終選擇使用經(jīng)過其他人仔細檢查和審核的非本人的哈希/加密庫。
- 不要將密碼輸出到日志中!
- 使用某種形式的密鑰管理服務。
- 不要將密鑰(API 密鑰、密碼)提交到代碼存儲庫中。
在本文中,我將通過一個示例應用程序來重點介紹加密關鍵數(shù)據(jù)的方式。對于本文中涉及的密碼存儲,我們將使用一個 SQLite 數(shù)據(jù)庫,因為它幾乎可以在任何系統(tǒng)上輕松使用。幾乎所有地方都使用著相同的原則和理念,而且數(shù)據(jù)庫系統(tǒng)應該無關緊要(但根據(jù)所選的數(shù)據(jù)庫,可能存在對用戶信息執(zhí)行哈希運算和保護的更好方法)。我還想展示,如果您丟失了數(shù)據(jù)庫文件,但仍保持用戶哈希值完整且無法破解,結果會怎樣?
使用 bcrypt
bcrypt 是目前對密碼執(zhí)行哈希運算的最廣泛使用的函數(shù)之一。它適用于大部分編程語言,而且通常有一些可用于特定框架和數(shù)據(jù)庫的非常特殊的模塊。讓我們看看這個存儲庫示例。此代碼通常與 Node.js 一起使用,而且非常簡單(它允許采用 sync 或 async 的方式來調用加鹽和哈希函數(shù))。它還使您無需擔心實現(xiàn)細節(jié)和加鹽過程,使您能專注于防止意外的密碼泄露。
哈希運算、鹽和加密是什么?
盡管哈希運算和加密看起來可能沒什么不同,而且可以互換使用,但它們實際上有很大區(qū)別,而且有不同的用例。哈希函數(shù)接受一些輸入,并對輸出進行單向映射。雖然有眾多的哈希技術和算法,但我推薦對密碼使用 bcrypt??梢栽诖颂庍M一步了解加密哈希函數(shù),但通常不必了解這些函數(shù)的基礎細節(jié)。在執(zhí)行哈希運算期間使用了鹽,將鹽作為提供給哈希函數(shù)的附加信息,使您(意外或通過暴力)即使找到一個哈希值,也無法校驗其他可能具有類似輸入的哈希值。例如,user_1 有一個與 user_2 的密碼相同的密碼。如果哈希函數(shù)中使用了鹽,這兩個用戶的密碼就無法被找到。要進一步了解此函數(shù),此處提供了各種各樣的信息和示例。
加密也是某個輸入與一個輸出之間的一對一映射。一個重要的關鍵區(qū)別是,如果您擁有加密密鑰,那么加密是可逆的。
您可以在以后使用哈希運算來檢查一個輸入與另一個輸入的映射,但您可能并不想直接存儲該輸入(密碼、pin 編號等)。在發(fā)送消息時(雙方都有一個用于編碼/解碼的密鑰),或者在您想存儲一些隱私信息(比如家庭地址或信用卡),但需要在以后通過某種方式檢索此信息時,可以使用加密。
前端
因為本文的重點不是前端,所以我們不打算采用任何會增加復雜性的內容或引入另一個令人擔憂的框架。我們將在同一個頁面上采用兩個用于登錄/注冊的表單。除了使用超級簡單的引導指令外,我們不會對這些表單執(zhí)行任何操作,因為這不是本文的重點。
- <form action="/signin" method="post">
- <div class="row">
- <div class="col">
- <input name="email" type="email" class="form-control" placeholder="email"/>
- </div>
- <div class="col">
- <input name="password" type="password" class="form-control" placeholder="password"/>
- </div>
- <div class="col">
- <button class="btn btn-dark">sign in</button>
- </div>
- </form>
- <form action="/register" method="post">
- <div class="row">
- <div class="col">
- <input name="email" type="email" class="form-control" placeholder="email"/>
- </div>
- <div class="col">
- <input name="password" type="password" class="form-control" placeholder="password"/>
- </div>
- <div class="col">
- <button class="btn btn-dark">register</button>
- </div>
- </div>
- </form>
我們還將輸入從表單提交到后端,而且不打算校驗/創(chuàng)建/設置會話,因為這不屬于本文的討論范圍,而且根據(jù)應用程序的目標或目的,涉及的內容可能很廣泛。
創(chuàng)建后端
接下來,我們將在 Node.js 中運行后端,方法是使用 Express 框架和 SQLite 來實現(xiàn)本文所需的最基本的系統(tǒng)。
- const path = require('path')
- const bcrypt = require('bcrypt')
- const bodyParser = require('body-parser')
- const sqlite = require('sqlite')
- const express = require('express')
- const app = express()
- app.use(bodyParser.json())
- app.use(bodyParser.urlencoded({ extended: true }))
- const dbPromise = sqlite.open('./database.sqlite', { Promise })
- const saltRounds = 10
我們在這里執(zhí)行的操作包括:為數(shù)據(jù)庫創(chuàng)建一個 promise,生成一個鹽,并創(chuàng)建應用程序和簡單中間件來獲取用戶名/密碼,加載一些我們想要使用的庫。
路徑
對于我們的服務器將要執(zhí)行的操作,我們將有一個登錄路徑和一個供用戶進行注冊的路徑。為了理解系統(tǒng)中正在發(fā)生的事情,我們將這兩條路徑分開了,但它們不會執(zhí)行任何操作(與會話/cookie 等相關的任何操作)。一旦密碼匹配,我們將(非常簡單地)展示如何對一個密碼執(zhí)行哈希運算,然后執(zhí)行校驗。登錄路徑與注冊路徑幾乎是相同的,盡管我們會在該 HTML 表單上檢查電子郵件,但我們不會在任何路徑上執(zhí)行任何數(shù)據(jù)驗證。
- app.get('/', async (req,res) => {
- res.sendFile(path.join(__dirname, '/main.html'))
- })
- app.post('/register', async (req, res) => {
- const db = await dbPromise
- // check if user already exists
- const checkUser = await db.get('SELECT * FROM Users WHERE email = ?', req.body.email)
- if (checkUser) {
- return res.send('user already exists')
- }
- const hashedPassword = await bcrypt.hash(req.body.password, saltRounds)
- const resp = await db.run(`INSERT INTO Users VALUES(?,?)`, req.body.email, hashedPassword)
- res.send('registered')
- })
注冊路徑檢查用戶是否存在于數(shù)據(jù)庫中,以及我們是否已使用一個經(jīng)過哈希運算的密碼將其插入數(shù)據(jù)庫中。請記住,我們不會執(zhí)行任何操作來減少 SQL 注入或其他各種形式的攻擊/濫用。如果該用戶不存在,我們會使用 bcrypt 哈希函數(shù)對密碼執(zhí)行哈希運算,該函數(shù)會在密碼中添加鹽,因為我們向鹽提供了運算的輪數(shù)。這種哈希運算使我們能夠以這樣一種方式存儲用戶的密碼 - 將來,如果用戶輸入了密碼,我們就可以檢查密碼。我們自己無法查找該密碼。另外,我們不應將密碼輸出到用戶的日志中,而且我們可能希望能夠使用數(shù)據(jù)庫模型來檢查密碼,并將用戶的密碼保存到哈希值中。
盡管登錄路徑幾乎相同(而且我們可以輕松重構此路徑來讓它更 DRY,但在這里提供它是為了便于理解),但有一行稍有不同:
- const passwordMatch = await bcrypt.compare(req.body.password, user.password)
此代碼使用 bcrypt 將經(jīng)過哈希運算的密碼與用戶在前端輸入的密碼進行比較,并返回 true 或 false。因為鹽已合并到哈希值中,所以我們不需要顯式使用它來進行比較。下面是要運行的完整的 server.js:
盡管登錄路徑幾乎相同(而且我們可以輕松地重構此路徑來讓它更 DRY,但在這里提供它是為了便于理解),但有一行稍有不同:
- const passwordMatch = await bcrypt.compare(req.body.password, user.password)
上面這行使用 bcrypt 將經(jīng)過哈希運算的密碼與用戶在前端輸入的密碼進行比較,并返回 true 或 false。因為鹽已合并到哈希值中,所以我們不需要顯式使用它來進行比較。下面的代碼清單是要運行的完整的 server.js:
- const bcrypt = require('bcrypt')
- const bodyParser = require('body-parser')
- const express = require('express')
- const app = express()
- app.post('/register', async (req, res) => {
- const db = await dbPromise
- const hashedPassword = await bcrypt.hash(req.body.password, saltRounds)
- const resp = await db.run(`INSERT INTO Users VALUES(?,?)`, req.body.email, hashedPassword)
- res.send('registered')
- })
- app.post('/signin', async (req, res) => {
- const db = await dbPromise
- const user = await db.get('SELECT * FROM Users WHERE email = ?', req.body.email)
- if (!user) {
- return res.send('user doesnt exist')
- }
- const passwordMatch = await bcrypt.compare(req.body.password, user.password)
- if (passwordMatch) {
- return res.send('signed in')
- }
- res.send('password does not match')
- })
- app.listen(PORT, async () => {
- console.log(`app listening at http://localhost:${PORT}`)
- })
現(xiàn)在安裝依賴項:
- yarn add bcrypt express body-parser sqlite。
運行服務器 Node server.js,打開 http://localhost:8080。然后嘗試登錄,創(chuàng)建一個用戶,并再次登錄。
通過網(wǎng)絡發(fā)送未加密的密碼!
盡管本文僅展示了如何存儲密碼并對其執(zhí)行哈希運算,而且您不會保存用戶的明文密碼,但我們仍在瀏覽器與后端之間發(fā)送明文,因為我們沒有使用 HTTPS。如果將此示例用在生產(chǎn)環(huán)境中,當黑客進入此通信渠道時,他們很容易看到在服務器與客戶端之間發(fā)送的密碼(包括登錄和注冊密碼)。有許多不同的方法可用來實際阻止中間人攻擊,但為了簡單起見,我們將在 Express 中處理它,生成自簽名 SSL 證書作為示例,以說明此工作原理。請記住,這些證書的簽署方式與從 LetsEncrypt 或其他各種 SSL/TLS 證書提供者獲取證書的方式不同。
首先,我們需要通過包管理器或通過 OpenSSL 的官方網(wǎng)站安裝 OpenSSL。在 macOS 上,如果您已安裝 homebrew,可以簡單寫入以下代碼:
- brew-install Openssl
接下來,需要運行以下命令來生成一個密鑰和一個證書:
- openSSL req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 30
此命令會要求您輸入一些信息,但在最后,您將獲得一個 key.pem 和一個 cert.pem。有了這兩個文件,就可以將以下代碼添加到 server.js 的頂部(請注意,我們現(xiàn)在使用的是來自 Node.js 的 https 標準庫):
- const fs = require('fs')
- const https = require('https')
- const options = {
- key: fs.readFileSync('key.pem'),
- cert: fs.readFileSync('cert.pem')
- }
在我們的代碼底部,以前包含以下代碼:
- const PORT = 8080
- app.listen(PORT, async () => {
- const db = await dbPromise
- await db.run("CREATE TABLE IF NOT EXISTS Users (email TEXT, password TEXT)")
- console.log(`app listening at http://localhost:${PORT}`)
- })
我們將上述以前的代碼更改為:
- const PORT = 8081
- https.createServer(options, app)
- .listen(PORT, async () => {
- const db = await dbPromise
- await db.run("CREATE TABLE IF NOT EXISTS Users (email TEXT, password TEXT)")
- console.log(`app listening at https://localhost:${PORT}`)
- })
此刻,我們將僅使用 HTTPS 并將加密后的密碼發(fā)送到服務器,而且會在將密碼保存到數(shù)據(jù)庫時執(zhí)行哈希運算。
最糟的情況:數(shù)據(jù)庫被泄露
設想我們的服務器被黑客攻擊,或者出現(xiàn)了其他一些漏洞,而且我們的 SQLite(或任何數(shù)據(jù)庫)被泄露。盡管這種情況很糟糕,但我們至少可以確信,用戶密碼本身應該是安全的,不會被使用,而且我們最大限度降低了從其他地方要求用戶更改密碼的可能性。 例如,圖 1 顯示,除非看到用戶 graham@test.xyz 的密碼 secret,否則哈希值對嘗試使用它的黑客毫無用處。
結束語:其他替代性云安全方法