Skip to content

Commit 131620d

Browse files
Jan Kopcsekjkopcsek
authored andcommitted
separate _users and sl-users hashing, make _users hashing compatible with couch34
1 parent 2fea83c commit 131620d

File tree

9 files changed

+181
-86
lines changed

9 files changed

+181
-86
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
## Change Log
22

3+
#### 0.22.X: Hashing update
4+
5+
##### 0.22.0
6+
- :sparkles: separate hashing configuration of `_users` and `sl-users` passwords (there is a new `sessionHashing` object right besides `iterations`)
7+
- :sparkles: session validation takes all parameters of the `_users` doc into account when verifying the session (`iterations` and `pbkdf2_prf`)
8+
- :sparkles: new `_users` session will default to 'sha256' with 1000 iterations and 32 byte keys. To prevent couchdb from upgrading them to stronger hashes, configure `upgrade_hash_on_auth` to false or `iterations` to 1000. As the `_users` provide only temporary access and use random passwords, the time to verify the hashes with 600.000 iterations it not really worth it as couch-auth doesn't employ hash-caching as couchdb does.
9+
310
#### 0.20.X: Brute force protection
411

512
##### 0.20.1

src/dbauth/couchdb.ts

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,28 @@
22
import { DocumentScope, ServerScope } from 'nano';
33
import {
44
getSecurityDoc,
5-
hyphenizeUUID,
65
putSecurityDoc,
76
toArray
87
} from '../util';
9-
import { hashCouchPassword, Hashing } from '../hashing';
8+
109
import { Config } from '../types/config';
1110
import { CouchDbAuthDoc } from '../types/typings';
1211
import { DBAdapter } from '../types/adapters';
12+
import { SessionHashing } from '../session-hashing';
1313

1414
const userPrefix = 'org.couchdb.user:';
1515

1616
export class CouchAdapter implements DBAdapter {
1717
couchAuthOnCloudant = false;
18-
private hasher: Hashing;
1918
constructor(
2019
private couchAuthDB: DocumentScope<CouchDbAuthDoc>,
2120
private couch: ServerScope,
22-
private config: Partial<Config>
21+
private config: Partial<Config>,
22+
private session: SessionHashing = new SessionHashing(config)
2323
) {
2424
if (this.config?.dbServer.couchAuthOnCloudant) {
2525
this.couchAuthOnCloudant = true;
2626
}
27-
this.hasher = new Hashing(config);
2827
}
2928

3029
/**
@@ -47,22 +46,16 @@ export class CouchAdapter implements DBAdapter {
4746
roles = [];
4847
}
4948
roles.unshift('user:' + username);
50-
let newKey: CouchDbAuthDoc = {
49+
const newKey: CouchDbAuthDoc = {
5150
_id: userPrefix + key,
5251
type: 'user',
5352
name: key,
5453
user_uid: user_uid,
5554
user_id: username,
5655
expires: expires,
5756
roles: roles,
58-
provider: provider
59-
};
60-
// required when using Cloudant or other db than `_users`
61-
newKey.password_scheme = 'pbkdf2';
62-
newKey.iterations = 10;
63-
newKey = {
64-
...newKey,
65-
...(await hashCouchPassword(password))
57+
provider: provider,
58+
...(await this.session.hashSessionPassword(password))
6659
};
6760

6861
try {

src/session-hashing.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
'use strict';
2+
import pwdModule from '@sl-nx/couch-pwd';
3+
import { Config } from './types/config';
4+
import { HashResult } from './types/typings';
5+
6+
export class SessionHashing {
7+
8+
static invalidErr = { status: 401, message: 'invalid token' };
9+
10+
// Hasher for hashing _users passwords
11+
private pwdCouch: pwdModule;
12+
13+
constructor(config: Partial<Config>) {
14+
const iterations = config.security?.sessionHashing?.iterations || 1000;
15+
const pbkdf2Prf = config.security?.sessionHashing?.pbkdf2Prf || 'sha256';
16+
const keyLength = config.security?.sessionHashing?.keyLength || 32;
17+
const saltLength = config.security?.sessionHashing?.saltLength || 16;
18+
19+
this.pwdCouch = new pwdModule(
20+
iterations,
21+
keyLength,
22+
saltLength,
23+
'hex',
24+
pbkdf2Prf
25+
);
26+
}
27+
28+
// Function for hashing _users passwords
29+
public hashSessionPassword(password: string): Promise<HashResult> {
30+
return new Promise((resolve, reject) => {
31+
this.pwdCouch.hash(password, (err, salt, hash) => {
32+
if (err) {
33+
return reject(err);
34+
}
35+
return resolve({
36+
salt: salt,
37+
derived_key: hash,
38+
password_scheme: 'pbkdf2',
39+
pbkdf2_prf: this.pwdCouch.digest,
40+
iterations: this.pwdCouch.iterations
41+
});
42+
});
43+
});
44+
}
45+
46+
public verifySessionPassword(hashObj: HashResult, pw: string): Promise<boolean> {
47+
return new Promise((resolve, reject) => {
48+
const iterations = hashObj.iterations || 10;
49+
const digest = hashObj.pbkdf2_prf || 'sha1';
50+
const length = digest === 'sha1' ? 20 : 32;
51+
const pwdCouch = new pwdModule(iterations, length, 16, 'hex', digest);
52+
53+
const salt = hashObj.salt;
54+
const derived_key = hashObj.derived_key;
55+
pwdCouch.hash(pw, salt, (err, hash) => {
56+
if (err) {
57+
return reject(err);
58+
} else if (hash !== derived_key) {
59+
return resolve(false);
60+
} else {
61+
return resolve(true);
62+
}
63+
});
64+
});
65+
}
66+
}

src/session.ts

Lines changed: 0 additions & 28 deletions
This file was deleted.

src/types/config.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,16 @@ export interface SecurityConfig {
9393
* next timestamp in the array. Default: `undefined` uses only 10 iterations.
9494
*/
9595
iterations?: [number, number][];
96+
sessionHashing?: {
97+
/** Hashing algorithm for pbkdf2, either 'sha1' or 'sha256'. Default: 'sha256' */
98+
pbkdf2Prf?: 'sha1' | 'sha256';
99+
/** Number of iterations for pbkdf2 password hashing. Default: 1000 */
100+
iterations?: number;
101+
/** Length of the derived key. Default: 32 */
102+
keyLength?: number;
103+
/** Length of the salt. Default: 16 */
104+
saltLength?: number;
105+
}
96106
/**
97107
* Whether couch-auth should handle errors itself or
98108
* forward to the express error mechanism (`next(err)`)

src/types/typings.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,24 @@ export interface HashResult {
5757
* (`security.iterations`) that matches the creation date.
5858
*/
5959
created?: number;
60+
61+
/**
62+
* The password hashing scheme used (e.g., 'pbkdf2')
63+
*/
64+
password_scheme?: string;
65+
/**
66+
* The pseudorandom function used for PBKDF2 hashing (e.g., 'sha1' or 'sha256')
67+
*/
68+
pbkdf2_prf?: string;
69+
70+
/**
71+
* Number of iterations used in the hashing process
72+
*/
73+
iterations?: number;
6074
}
6175

6276
export interface LocalHashObj extends HashResult {
6377
failedLoginAttempts?: number;
64-
iterations?: number;
6578
lockedUntil?: number;
6679
}
6780

src/hashing.ts renamed to src/user-hashing.ts

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,10 @@ import { URLSafeUUID } from './util';
55

66
const pwd = new pwdModule();
77

8-
export function hashCouchPassword(password: string): Promise<HashResult> {
9-
return new Promise(function (resolve, reject) {
10-
pwd.hash(password, function (err, salt, hash) {
11-
if (err) {
12-
return reject(err);
13-
}
14-
return resolve({
15-
salt: salt,
16-
derived_key: hash
17-
});
18-
});
19-
});
20-
}
21-
export class Hashing {
8+
/**
9+
* Class for hashing and verifying sl-user passwords
10+
*/
11+
export class UserHashing {
2212
hashers = [];
2313
times: number[] = [];
2414
dummyHashObject: LocalHashObj = { iterations: 10 };

src/user.ts

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import { DocumentScope, ServerScope } from 'nano';
77
import url from 'url';
88
import { v4 as uuidv4 } from 'uuid';
99
import { DBAuth } from './dbauth';
10-
import { Hashing } from './hashing';
10+
import { UserHashing } from './user-hashing';
1111
import { Mailer } from './mailer';
12-
import { Session } from './session';
12+
import { SessionHashing } from './session-hashing';
1313
import { Config } from './types/config';
1414
import {
1515
ConsentRequest,
@@ -50,10 +50,10 @@ export enum ValidErr {
5050
export class User {
5151
private dbAuth: DBAuth;
5252
private userDbManager: DbManager;
53-
private session: Session;
53+
private session: SessionHashing;
5454
private onCreateActions: SlAction[];
5555
private onLinkActions: SlAction[];
56-
private hasher: Hashing;
56+
private hasher: UserHashing;
5757

5858
private passwordConstraints;
5959
/**
@@ -89,8 +89,8 @@ export class User {
8989
this.dbAuth = new DBAuth(config, userDB, couchServer, couchAuthDB);
9090
this.onCreateActions = [];
9191
this.onLinkActions = [];
92-
this.hasher = new Hashing(config);
93-
this.session = new Session(this.hasher);
92+
this.hasher = new UserHashing(config);
93+
this.session = new SessionHashing(config);
9494
this.userDbManager = new DbManager(userDB, config);
9595
this.passwordConstraints = config.local.passwordConstraints;
9696

@@ -1341,26 +1341,25 @@ export class User {
13411341
}
13421342

13431343
if (doc.expires > Date.now()) {
1344-
doc._id = doc.user_id;
1345-
delete doc.user_id;
1346-
delete doc.name;
1347-
delete doc.type;
1348-
delete doc._rev;
1349-
delete doc.password_scheme;
1350-
return { key, ...(await this.session.confirmToken(doc, password)) } as {
1351-
key: string;
1352-
user_uid: string;
1353-
expires: number;
1354-
roles: string[];
1355-
provider: string;
1356-
};
1344+
if (await this.session.verifySessionPassword(doc, password)) {
1345+
return {
1346+
key,
1347+
_id: doc.user_id,
1348+
user_uid: doc.user_uid,
1349+
expires: doc.expires,
1350+
roles: doc.roles,
1351+
provider: doc.provider
1352+
};
1353+
} else {
1354+
throw SessionHashing.invalidErr;
1355+
}
13571356
} else {
13581357
this.dbAuth.removeKeys(key);
1358+
throw SessionHashing.invalidErr;
13591359
}
13601360
} catch {
1361-
await this.session.confirmToken({}, password);
1361+
throw SessionHashing.invalidErr;
13621362
}
1363-
throw Session.invalidErr;
13641363
}
13651364

13661365
/**

0 commit comments

Comments
 (0)