Implement admin password hashing with bcrypt (#3754)
* Add bcrypt hashing helpers * SetAdminPassword now hashes the password before saving it * BasicAuth now compares the bcrypt hash for the password * Modify migration2 to avoid a double password hash when upgrading * Add migration for bcrypt hashed password * Do not show admin password hash as initial value * Update api tests to compare the bcrypt hash of the admin password instead * Remove old admin password api tests --------- Co-authored-by: Gabe Kangas <gabek@real-ity.com>
This commit is contained in:
parent
51cd16dcc1
commit
a7e5f20337
core/data
router/middleware
test/automated/api
utils
web/components/admin
@ -115,7 +115,11 @@ func GetAdminPassword() string {
|
||||
|
||||
// SetAdminPassword will set the admin password.
|
||||
func SetAdminPassword(key string) error {
|
||||
return _datastore.SetString(adminPasswordKey, key)
|
||||
hashed_pass, err := utils.HashPassword(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return _datastore.SetString(adminPasswordKey, hashed_pass)
|
||||
}
|
||||
|
||||
// GetLogoPath will return the path for the logo, relative to webroot.
|
||||
|
@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
datastoreValuesVersion = 3
|
||||
datastoreValuesVersion = 4
|
||||
datastoreValueVersionKey = "DATA_STORE_VERSION"
|
||||
)
|
||||
|
||||
@ -27,6 +27,8 @@ func migrateDatastoreValues(datastore *Datastore) {
|
||||
migrateToDatastoreValues2(datastore)
|
||||
case 2:
|
||||
migrateToDatastoreValues3ServingEndpoint3(datastore)
|
||||
case 3:
|
||||
migrateToDatastoreValues4(datastore)
|
||||
default:
|
||||
log.Fatalln("missing datastore values migration step")
|
||||
}
|
||||
@ -58,7 +60,8 @@ func migrateToDatastoreValues1(datastore *Datastore) {
|
||||
|
||||
func migrateToDatastoreValues2(datastore *Datastore) {
|
||||
oldAdminPassword, _ := datastore.GetString("stream_key")
|
||||
_ = SetAdminPassword(oldAdminPassword)
|
||||
// Avoids double hashing the password
|
||||
_ = datastore.SetString("admin_password_key", oldAdminPassword)
|
||||
_ = SetStreamKeys([]models.StreamKey{
|
||||
{Key: oldAdminPassword, Comment: "Default stream key"},
|
||||
})
|
||||
@ -73,3 +76,11 @@ func migrateToDatastoreValues3ServingEndpoint3(_ *Datastore) {
|
||||
|
||||
_ = SetVideoServingEndpoint(s3Config.ServingEndpoint)
|
||||
}
|
||||
|
||||
func migrateToDatastoreValues4(datastore *Datastore) {
|
||||
unhashed_pass, _ := datastore.GetString("admin_password_key")
|
||||
err := SetAdminPassword(unhashed_pass)
|
||||
if err != nil {
|
||||
log.Fatalln("error migrating admin password:", err)
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ func RequireAdminAuth(handler http.HandlerFunc) http.HandlerFunc {
|
||||
user, pass, ok := r.BasicAuth()
|
||||
|
||||
// Failed
|
||||
if !ok || subtle.ConstantTimeCompare([]byte(user), []byte(username)) != 1 || subtle.ConstantTimeCompare([]byte(pass), []byte(password)) != 1 {
|
||||
if !ok || subtle.ConstantTimeCompare([]byte(user), []byte(username)) != 1 || utils.ComparseHash(password, pass) != nil {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="`+realm+`"`)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
log.Debugln("Failed admin authentication")
|
||||
|
@ -1,4 +1,5 @@
|
||||
var request = require('supertest');
|
||||
var bcrypt = require('bcrypt');
|
||||
|
||||
const sendAdminRequest = require('./lib/admin').sendAdminRequest;
|
||||
const failAdminRequest = require('./lib/admin').failAdminRequest;
|
||||
@ -166,7 +167,9 @@ test('verify default admin configuration', async (done) => {
|
||||
expect(res.body.yp.enabled).toBe(defaultYPConfig.enabled);
|
||||
// expect(res.body.yp.instanceUrl).toBe(defaultYPConfig.instanceUrl);
|
||||
|
||||
expect(res.body.adminPassword).toBe(defaultAdminPassword);
|
||||
bcrypt.compare(defaultAdminPassword, res.body.adminPassword, function (err, result) {
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
expect(res.body.s3.enabled).toBe(defaultS3Config.enabled);
|
||||
expect(res.body.s3.forcePathStyle).toBe(defaultS3Config.forcePathStyle);
|
||||
@ -374,7 +377,9 @@ test('verify admin password change', async (done) => {
|
||||
(adminPassword = newAdminPassword)
|
||||
);
|
||||
|
||||
expect(res.body.adminPassword).toBe(newAdminPassword);
|
||||
bcrypt.compare(newAdminPassword, res.body.adminPassword, function(err, result) {
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
done();
|
||||
});
|
||||
|
||||
@ -448,7 +453,9 @@ test('verify updated admin configuration', async (done) => {
|
||||
expect(res.body.yp.enabled).toBe(newYPConfig.enabled);
|
||||
// expect(res.body.yp.instanceUrl).toBe(newYPConfig.instanceUrl);
|
||||
|
||||
expect(res.body.adminPassword).toBe(defaultAdminPassword);
|
||||
bcrypt.compare(defaultAdminPassword, res.body.adminPassword, function(err, result) {
|
||||
expect(result).toBe(true);
|
||||
})
|
||||
|
||||
expect(res.body.s3.enabled).toBe(newS3Config.enabled);
|
||||
expect(res.body.s3.endpoint).toBe(newS3Config.endpoint);
|
||||
|
3497
test/automated/api/package-lock.json
generated
3497
test/automated/api/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -9,12 +9,13 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"supertest": "^6.3.2",
|
||||
"websocket": "^1.0.32",
|
||||
"ajv": "^8.11.0",
|
||||
"ajv-draft-04": "^1.0.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"crypto-random": "^2.0.1",
|
||||
"jsonfile": "^6.1.0",
|
||||
"crypto-random": "^2.0.1"
|
||||
"supertest": "^6.3.2",
|
||||
"websocket": "^1.0.32"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^29.7.0",
|
||||
|
15
utils/hashing.go
Normal file
15
utils/hashing.go
Normal file
@ -0,0 +1,15 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func HashPassword(password string) (string, error) {
|
||||
// 0 will use the default cost of 10 instead
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), 0)
|
||||
return string(hash), err
|
||||
}
|
||||
|
||||
func ComparseHash(hash string, password string) error {
|
||||
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
}
|
@ -26,7 +26,6 @@ export default function EditInstanceDetails() {
|
||||
const { serverConfig } = serverStatusData || {};
|
||||
|
||||
const {
|
||||
adminPassword,
|
||||
ffmpegPath,
|
||||
rtmpServerPort,
|
||||
webServerPort,
|
||||
@ -37,7 +36,6 @@ export default function EditInstanceDetails() {
|
||||
|
||||
useEffect(() => {
|
||||
setFormDataValues({
|
||||
adminPassword,
|
||||
ffmpegPath,
|
||||
rtmpServerPort,
|
||||
webServerPort,
|
||||
@ -81,7 +79,6 @@ export default function EditInstanceDetails() {
|
||||
fieldName="adminPassword"
|
||||
{...TEXTFIELD_PROPS_ADMIN_PASSWORD}
|
||||
value={formDataValues.adminPassword}
|
||||
initialValue={adminPassword}
|
||||
type={TEXTFIELD_TYPE_PASSWORD}
|
||||
onChange={handleFieldChange}
|
||||
onSubmit={showStreamKeyChangeMessage}
|
||||
|
Loading…
x
Reference in New Issue
Block a user