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
@ -115,7 +115,11 @@ func GetAdminPassword() string {
|
|||||||
|
|
||||||
// SetAdminPassword will set the admin password.
|
// SetAdminPassword will set the admin password.
|
||||||
func SetAdminPassword(key string) error {
|
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.
|
// GetLogoPath will return the path for the logo, relative to webroot.
|
||||||
|
@ -8,7 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
datastoreValuesVersion = 3
|
datastoreValuesVersion = 4
|
||||||
datastoreValueVersionKey = "DATA_STORE_VERSION"
|
datastoreValueVersionKey = "DATA_STORE_VERSION"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -27,6 +27,8 @@ func migrateDatastoreValues(datastore *Datastore) {
|
|||||||
migrateToDatastoreValues2(datastore)
|
migrateToDatastoreValues2(datastore)
|
||||||
case 2:
|
case 2:
|
||||||
migrateToDatastoreValues3ServingEndpoint3(datastore)
|
migrateToDatastoreValues3ServingEndpoint3(datastore)
|
||||||
|
case 3:
|
||||||
|
migrateToDatastoreValues4(datastore)
|
||||||
default:
|
default:
|
||||||
log.Fatalln("missing datastore values migration step")
|
log.Fatalln("missing datastore values migration step")
|
||||||
}
|
}
|
||||||
@ -58,7 +60,8 @@ func migrateToDatastoreValues1(datastore *Datastore) {
|
|||||||
|
|
||||||
func migrateToDatastoreValues2(datastore *Datastore) {
|
func migrateToDatastoreValues2(datastore *Datastore) {
|
||||||
oldAdminPassword, _ := datastore.GetString("stream_key")
|
oldAdminPassword, _ := datastore.GetString("stream_key")
|
||||||
_ = SetAdminPassword(oldAdminPassword)
|
// Avoids double hashing the password
|
||||||
|
_ = datastore.SetString("admin_password_key", oldAdminPassword)
|
||||||
_ = SetStreamKeys([]models.StreamKey{
|
_ = SetStreamKeys([]models.StreamKey{
|
||||||
{Key: oldAdminPassword, Comment: "Default stream key"},
|
{Key: oldAdminPassword, Comment: "Default stream key"},
|
||||||
})
|
})
|
||||||
@ -73,3 +76,11 @@ func migrateToDatastoreValues3ServingEndpoint3(_ *Datastore) {
|
|||||||
|
|
||||||
_ = SetVideoServingEndpoint(s3Config.ServingEndpoint)
|
_ = 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()
|
user, pass, ok := r.BasicAuth()
|
||||||
|
|
||||||
// Failed
|
// 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+`"`)
|
w.Header().Set("WWW-Authenticate", `Basic realm="`+realm+`"`)
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
log.Debugln("Failed admin authentication")
|
log.Debugln("Failed admin authentication")
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
var request = require('supertest');
|
var request = require('supertest');
|
||||||
|
var bcrypt = require('bcrypt');
|
||||||
|
|
||||||
const sendAdminRequest = require('./lib/admin').sendAdminRequest;
|
const sendAdminRequest = require('./lib/admin').sendAdminRequest;
|
||||||
const failAdminRequest = require('./lib/admin').failAdminRequest;
|
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.enabled).toBe(defaultYPConfig.enabled);
|
||||||
// expect(res.body.yp.instanceUrl).toBe(defaultYPConfig.instanceUrl);
|
// 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.enabled).toBe(defaultS3Config.enabled);
|
||||||
expect(res.body.s3.forcePathStyle).toBe(defaultS3Config.forcePathStyle);
|
expect(res.body.s3.forcePathStyle).toBe(defaultS3Config.forcePathStyle);
|
||||||
@ -374,7 +377,9 @@ test('verify admin password change', async (done) => {
|
|||||||
(adminPassword = newAdminPassword)
|
(adminPassword = newAdminPassword)
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(res.body.adminPassword).toBe(newAdminPassword);
|
bcrypt.compare(newAdminPassword, res.body.adminPassword, function(err, result) {
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -448,7 +453,9 @@ test('verify updated admin configuration', async (done) => {
|
|||||||
expect(res.body.yp.enabled).toBe(newYPConfig.enabled);
|
expect(res.body.yp.enabled).toBe(newYPConfig.enabled);
|
||||||
// expect(res.body.yp.instanceUrl).toBe(newYPConfig.instanceUrl);
|
// 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.enabled).toBe(newS3Config.enabled);
|
||||||
expect(res.body.s3.endpoint).toBe(newS3Config.endpoint);
|
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": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"supertest": "^6.3.2",
|
|
||||||
"websocket": "^1.0.32",
|
|
||||||
"ajv": "^8.11.0",
|
"ajv": "^8.11.0",
|
||||||
"ajv-draft-04": "^1.0.0",
|
"ajv-draft-04": "^1.0.0",
|
||||||
|
"bcrypt": "^5.1.1",
|
||||||
|
"crypto-random": "^2.0.1",
|
||||||
"jsonfile": "^6.1.0",
|
"jsonfile": "^6.1.0",
|
||||||
"crypto-random": "^2.0.1"
|
"supertest": "^6.3.2",
|
||||||
|
"websocket": "^1.0.32"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"jest": "^29.7.0",
|
"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 { serverConfig } = serverStatusData || {};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
adminPassword,
|
|
||||||
ffmpegPath,
|
ffmpegPath,
|
||||||
rtmpServerPort,
|
rtmpServerPort,
|
||||||
webServerPort,
|
webServerPort,
|
||||||
@ -37,7 +36,6 @@ export default function EditInstanceDetails() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFormDataValues({
|
setFormDataValues({
|
||||||
adminPassword,
|
|
||||||
ffmpegPath,
|
ffmpegPath,
|
||||||
rtmpServerPort,
|
rtmpServerPort,
|
||||||
webServerPort,
|
webServerPort,
|
||||||
@ -81,7 +79,6 @@ export default function EditInstanceDetails() {
|
|||||||
fieldName="adminPassword"
|
fieldName="adminPassword"
|
||||||
{...TEXTFIELD_PROPS_ADMIN_PASSWORD}
|
{...TEXTFIELD_PROPS_ADMIN_PASSWORD}
|
||||||
value={formDataValues.adminPassword}
|
value={formDataValues.adminPassword}
|
||||||
initialValue={adminPassword}
|
|
||||||
type={TEXTFIELD_TYPE_PASSWORD}
|
type={TEXTFIELD_TYPE_PASSWORD}
|
||||||
onChange={handleFieldChange}
|
onChange={handleFieldChange}
|
||||||
onSubmit={showStreamKeyChangeMessage}
|
onSubmit={showStreamKeyChangeMessage}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user