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:
mahmed2000 2024-06-27 09:20:22 +05:00 committed by GitHub
parent 51cd16dcc1
commit a7e5f20337
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 497 additions and 3061 deletions

View file

@ -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.

View file

@ -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)
}
}

View file

@ -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")

View file

@ -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);

File diff suppressed because it is too large Load diff

View file

@ -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
View 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))
}

View file

@ -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}