backend-plus 2.5.2-betha.27 → 2.5.2-betha.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/backend-plus.js +139 -60
- package/package.json +1 -1
package/lib/backend-plus.js
CHANGED
|
@@ -85,61 +85,115 @@ function md5(text){
|
|
|
85
85
|
|
|
86
86
|
const DEFAULT_ITERATIONS = 4096;
|
|
87
87
|
const HASH_ALGORITHM = 'sha256';
|
|
88
|
-
|
|
88
|
+
|
|
89
|
+
// primera version de scram sha 256. El Verifier completo tenía 64 bytes.
|
|
90
|
+
const LEGACY_KEY_LENGTH = 64;
|
|
91
|
+
|
|
92
|
+
// Formato nuevo/PG-Compatible: El Client Key tiene 32 bytes (SHA-256).
|
|
93
|
+
const SCRAM_KEY_LENGTH = 32;
|
|
94
|
+
|
|
89
95
|
const bufferToBase64 = (buffer) => buffer.toString('base64');
|
|
90
96
|
|
|
91
97
|
/**
|
|
92
|
-
* Función central para PBKDF2.
|
|
93
|
-
* Devuelve la clave derivada.
|
|
98
|
+
* Función central para PBKDF2 (Acepta KEY_LEN para compatibilidad).
|
|
99
|
+
* Devuelve la clave derivada con la longitud especificada.
|
|
94
100
|
*/
|
|
95
|
-
function deriveKey(password,
|
|
101
|
+
function deriveKey(password, saltBuffer, iterations, keyLength) {
|
|
96
102
|
return new Promise((resolve, reject) => {
|
|
97
103
|
crypto.pbkdf2(
|
|
98
104
|
password,
|
|
99
|
-
|
|
105
|
+
saltBuffer,
|
|
100
106
|
iterations,
|
|
101
|
-
|
|
107
|
+
keyLength,
|
|
102
108
|
HASH_ALGORITHM,
|
|
103
109
|
(err, derivedKey) => {
|
|
104
110
|
if (err) return reject(err);
|
|
105
|
-
resolve(derivedKey);
|
|
111
|
+
resolve(derivedKey);
|
|
106
112
|
}
|
|
107
113
|
);
|
|
108
114
|
});
|
|
109
115
|
}
|
|
110
116
|
|
|
117
|
+
async function generateScramVerifier(password) {
|
|
118
|
+
const saltBuffer = crypto.randomBytes(16);
|
|
119
|
+
const saltBase64 = bufferToBase64(saltBuffer);
|
|
120
|
+
|
|
121
|
+
const Hi = await deriveKey(
|
|
122
|
+
password,
|
|
123
|
+
saltBuffer,
|
|
124
|
+
DEFAULT_ITERATIONS,
|
|
125
|
+
SCRAM_KEY_LENGTH
|
|
126
|
+
);
|
|
127
|
+
const clientKey = crypto.createHmac('sha256', Hi).update('Client Key').digest();
|
|
128
|
+
const serverKey = crypto.createHmac('sha256', Hi).update('Server Key').digest();
|
|
129
|
+
|
|
130
|
+
const storedKey = crypto.createHash('sha256').update(clientKey).digest();
|
|
131
|
+
|
|
132
|
+
const storedKeyBase64 = bufferToBase64(storedKey);
|
|
133
|
+
const serverKeyBase64 = bufferToBase64(serverKey);
|
|
134
|
+
|
|
135
|
+
return `SCRAM-SHA-256$${DEFAULT_ITERATIONS}:${saltBase64}$${storedKeyBase64}:${serverKeyBase64}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
111
138
|
/**
|
|
112
|
-
*
|
|
113
|
-
* Formato: SCRAM-SHA-256
|
|
139
|
+
* Verifica una contraseña contra el hash SCRAM PG-Compatible.
|
|
140
|
+
* Formato PG: SCRAM-SHA-256$<iteraciones>:<Salt>$<StoredKey>:<ServerKey>
|
|
114
141
|
* @param {string} password - Contraseña en texto plano.
|
|
115
|
-
* @
|
|
142
|
+
* @param {string} storedScramString - Cadena SCRAM de PostgreSQL.
|
|
143
|
+
* @returns {Promise<boolean>} True si la contraseña es válida.
|
|
116
144
|
*/
|
|
117
|
-
async function
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
145
|
+
async function verifyScramPG(password, storedScramString) {
|
|
146
|
+
if (!storedScramString.startsWith('SCRAM-SHA-256$')) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const parts = storedScramString.split('$');
|
|
151
|
+
if (parts.length !== 3) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const [storedIterations, saltBase64] = parts[1].split(':');
|
|
156
|
+
const [storedKeyBase64, serverKeyBase64] = parts[2].split(':'); // Asume StoredKey y ServerKey
|
|
157
|
+
|
|
158
|
+
if (!saltBase64 || !storedKeyBase64 || !serverKeyBase64 || isNaN(parseInt(storedIterations))) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const iterations = parseInt(storedIterations);
|
|
163
|
+
const saltBuffer = Buffer.from(saltBase64, 'base64');
|
|
164
|
+
const storedKeyBuffer = Buffer.from(storedKeyBase64, 'base64');
|
|
165
|
+
const serverKeyBuffer = Buffer.from(serverKeyBase64, 'base64');
|
|
121
166
|
|
|
122
|
-
|
|
123
|
-
const verifierBuffer = await deriveKey(
|
|
167
|
+
const Hi = await deriveKey(
|
|
124
168
|
password,
|
|
125
|
-
|
|
126
|
-
|
|
169
|
+
saltBuffer,
|
|
170
|
+
iterations,
|
|
171
|
+
SCRAM_KEY_LENGTH
|
|
127
172
|
);
|
|
128
|
-
const verifierBase64 = bufferToBase64(verifierBuffer);
|
|
129
173
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
174
|
+
const clientKey = crypto.createHmac(HASH_ALGORITHM, Hi)
|
|
175
|
+
.update('Client Key')
|
|
176
|
+
.digest();
|
|
177
|
+
|
|
178
|
+
const serverKeyActual = crypto.createHmac(HASH_ALGORITHM, Hi)
|
|
179
|
+
.update('Server Key')
|
|
180
|
+
.digest();
|
|
181
|
+
|
|
182
|
+
const storedKeyActual = crypto.createHash('sha256').update(clientKey).digest();
|
|
183
|
+
|
|
184
|
+
const storedKeyMatch = crypto.timingSafeEqual(storedKeyActual, storedKeyBuffer);
|
|
185
|
+
const serverKeyMatch = crypto.timingSafeEqual(serverKeyActual, serverKeyBuffer);
|
|
186
|
+
return storedKeyMatch && serverKeyMatch;
|
|
134
187
|
}
|
|
135
188
|
|
|
136
189
|
/**
|
|
137
|
-
* Verifica una contraseña
|
|
138
|
-
*
|
|
139
|
-
* @param {string}
|
|
190
|
+
* Verifica una contraseña contra el hash SCRAM Legacy (64 bytes).
|
|
191
|
+
* Formato Legacy: SCRAM-SHA-256$<iteraciones>:<Salt>$<Verifier(64 bytes)>
|
|
192
|
+
* @param {string} password - Contraseña en texto plano.
|
|
193
|
+
* @param {string} storedScramString - Cadena SCRAM Legacy.
|
|
140
194
|
* @returns {Promise<boolean>} True si la contraseña es válida.
|
|
141
195
|
*/
|
|
142
|
-
async function
|
|
196
|
+
async function verifyScramLegacy(password, storedScramString) {
|
|
143
197
|
if (!storedScramString.startsWith('SCRAM-SHA-256$')) {
|
|
144
198
|
// No es formato SCRAM. Debe ser MD5 u otro algoritmo.
|
|
145
199
|
return false;
|
|
@@ -166,7 +220,8 @@ async function verifyScram(password, storedScramString) {
|
|
|
166
220
|
const generatedVerifierBuffer = await deriveKey(
|
|
167
221
|
password,
|
|
168
222
|
storedSalt,
|
|
169
|
-
iterations
|
|
223
|
+
iterations,
|
|
224
|
+
LEGACY_KEY_LENGTH
|
|
170
225
|
);
|
|
171
226
|
const generatedVerifier = bufferToBase64(generatedVerifierBuffer);
|
|
172
227
|
|
|
@@ -1119,6 +1174,32 @@ AppBackend.prototype.start = function start(opts){
|
|
|
1119
1174
|
mainApp.loginPlusManager.closeManager();
|
|
1120
1175
|
}
|
|
1121
1176
|
});
|
|
1177
|
+
const updatePassword = async ({client, username, password, setUpdateDate, errorIfNoResult}) => {
|
|
1178
|
+
const {table, passFieldName, userFieldName, passUpdatedAtFieldName, passAlgorithmFieldName, schema} = be.config.login;
|
|
1179
|
+
const hashPass = await generateScramVerifier(password);
|
|
1180
|
+
let params = [username, hashPass];
|
|
1181
|
+
let setters = [`${be.db.quoteIdent(passFieldName)} = $2`];
|
|
1182
|
+
if(passAlgorithmFieldName){
|
|
1183
|
+
setters.push(`${be.db.quoteIdent(passAlgorithmFieldName)} = $3`);
|
|
1184
|
+
params.push('PG-SHA256')
|
|
1185
|
+
}
|
|
1186
|
+
if(setUpdateDate && passUpdatedAtFieldName){
|
|
1187
|
+
setters.push(`${be.db.quoteIdent(passUpdatedAtFieldName)} = current_timestamp`)
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
const result = await client.query(`
|
|
1191
|
+
UPDATE ${(schema ? be.db.quoteIdent(schema) + '.' : '') + be.db.quoteIdent(table)}
|
|
1192
|
+
SET ${setters.join(', ')}
|
|
1193
|
+
WHERE ${be.db.quoteIdent(userFieldName)} = $1
|
|
1194
|
+
returning 1 as ok
|
|
1195
|
+
`, params
|
|
1196
|
+
).fetchOneRowIfExists();
|
|
1197
|
+
|
|
1198
|
+
if(result.rowCount === 0 && errorIfNoResult){
|
|
1199
|
+
throw Error ('no se encontró el usuario')
|
|
1200
|
+
};
|
|
1201
|
+
return result
|
|
1202
|
+
}
|
|
1122
1203
|
mainApp.loginPlusManager.setValidatorStrategy(
|
|
1123
1204
|
async function(req, username, password, done) {
|
|
1124
1205
|
try{
|
|
@@ -1152,36 +1233,42 @@ AppBackend.prototype.start = function start(opts){
|
|
|
1152
1233
|
done(null,false,{message:be.messages.unlogged.login.userOrPassFail});
|
|
1153
1234
|
return
|
|
1154
1235
|
}else{
|
|
1155
|
-
|
|
1236
|
+
let needsMigration = false;
|
|
1237
|
+
const user = data.row;
|
|
1238
|
+
if(!user[passFieldName]){
|
|
1239
|
+
done(null,false,{message:be.messages.unlogged.login.userOrPassFail});
|
|
1240
|
+
return
|
|
1241
|
+
}
|
|
1156
1242
|
const usaScramSha256 = user[passFieldName].startsWith('SCRAM-SHA-256$')
|
|
1157
1243
|
if (usaScramSha256){
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1244
|
+
let isScramValid = false;
|
|
1245
|
+
// 1. Intento con formato PG-Compatible (Nuevo)
|
|
1246
|
+
if(await verifyScramPG(password, user[passFieldName])){
|
|
1247
|
+
isScramValid = true;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
// 2. Intento con formato Legacy (64 bytes)
|
|
1251
|
+
else if(await verifyScramLegacy(password, user[passFieldName])){
|
|
1252
|
+
isScramValid = true;
|
|
1253
|
+
needsMigration = true;
|
|
1254
|
+
}
|
|
1162
1255
|
if(!isScramValid){
|
|
1163
1256
|
done(null,false,{message:be.messages.unlogged.login.userOrPassFail});
|
|
1164
1257
|
return
|
|
1165
1258
|
}
|
|
1166
1259
|
}else{
|
|
1167
|
-
if (md5(password+username.toLowerCase()) === user[passFieldName])
|
|
1168
|
-
|
|
1169
|
-
// Generar nuevos valores SCRAM
|
|
1170
|
-
const hashPass = await generateScramVerifier(password);
|
|
1171
|
-
//sobrescribo el hash MD5 antiguo
|
|
1172
|
-
await client.query(`
|
|
1173
|
-
UPDATE usuarios
|
|
1174
|
-
SET ${be.db.quoteIdent(passFieldName)} = $1
|
|
1175
|
-
WHERE usuario = $2
|
|
1176
|
-
returning *`,
|
|
1177
|
-
[hashPass, username]
|
|
1178
|
-
).fetchUniqueRow();
|
|
1179
|
-
console.log('Migración completada.');
|
|
1260
|
+
if (md5(password+username.toLowerCase()) === user[passFieldName]){
|
|
1261
|
+
needsMigration = true;
|
|
1180
1262
|
}else{
|
|
1181
1263
|
done(null,false,{message:be.messages.unlogged.login.userOrPassFail});
|
|
1182
1264
|
return
|
|
1183
1265
|
}
|
|
1184
1266
|
}
|
|
1267
|
+
if(needsMigration){
|
|
1268
|
+
console.log('Ejecutando migración a SCRAM...');
|
|
1269
|
+
await updatePassword({client, username, password, setUpdateDate:false, errorIfNoResult:true});
|
|
1270
|
+
console.log('Migración completada.');
|
|
1271
|
+
}
|
|
1185
1272
|
}
|
|
1186
1273
|
//continua validando
|
|
1187
1274
|
if(data.rowCount==1){
|
|
@@ -1234,8 +1321,7 @@ AppBackend.prototype.start = function start(opts){
|
|
|
1234
1321
|
}
|
|
1235
1322
|
);
|
|
1236
1323
|
be.changePassword=async function(client,username,oldPassword,newPassword){
|
|
1237
|
-
const { table, passFieldName, userFieldName } = be.config.login;
|
|
1238
|
-
const { schema } = be.config.db;
|
|
1324
|
+
const { table, passFieldName, userFieldName, schema } = be.config.login;
|
|
1239
1325
|
let ok = false;
|
|
1240
1326
|
const data = await client.query(
|
|
1241
1327
|
`SELECT *
|
|
@@ -1251,7 +1337,10 @@ AppBackend.prototype.start = function start(opts){
|
|
|
1251
1337
|
const storedHash = user[passFieldName];
|
|
1252
1338
|
if (oldPassword !== false) {
|
|
1253
1339
|
if (storedHash.startsWith('SCRAM-SHA-256$')) {//Intento SCRAM-SHA-256
|
|
1254
|
-
ok = await
|
|
1340
|
+
ok = await verifyScramPG(oldPassword, storedHash);
|
|
1341
|
+
if (!ok) {
|
|
1342
|
+
ok = await verifyScramLegacy(oldPassword, storedHash);
|
|
1343
|
+
}
|
|
1255
1344
|
} else { //Intento MD5
|
|
1256
1345
|
const md5Hash = md5(oldPassword + username.toLowerCase());
|
|
1257
1346
|
ok = (md5Hash === storedHash);
|
|
@@ -1266,17 +1355,7 @@ AppBackend.prototype.start = function start(opts){
|
|
|
1266
1355
|
}
|
|
1267
1356
|
// Si la verificación fue exitosa o se omitió (ok == true), generamos el nuevo hash SCRAM
|
|
1268
1357
|
if (ok) {
|
|
1269
|
-
|
|
1270
|
-
const newScramVerifier = await generateScramVerifier(newPassword);
|
|
1271
|
-
const updateSql =
|
|
1272
|
-
"UPDATE " + (schema ? be.db.quoteIdent(schema) + '.' : '') +
|
|
1273
|
-
be.db.quoteIdent(table) +
|
|
1274
|
-
"\n SET " + be.db.quoteIdent(passFieldName) + " = $2 " +
|
|
1275
|
-
"\n WHERE " + be.db.quoteIdent(userFieldName) + " = $1 " +
|
|
1276
|
-
"\n RETURNING 1 as ok";
|
|
1277
|
-
|
|
1278
|
-
const updateParams = [username, newScramVerifier];
|
|
1279
|
-
const updateResult = await client.query(updateSql, updateParams).fetchOneRowIfExists();
|
|
1358
|
+
const updateResult = await updatePassword({client, username, password:newPassword, setUpdateDate:true, errorIfNoResult:false});
|
|
1280
1359
|
ok = updateResult.rowCount === 1;
|
|
1281
1360
|
if (ok && be.config.login['double-dragon']) {
|
|
1282
1361
|
const doubleDragonSql = "ALTER USER " + be.db.quoteIdent(username) + " WITH PASSWORD " + be.db.quoteLiteral(newPassword);
|
package/package.json
CHANGED