backend-plus 2.5.2-betha.27 → 2.5.2-betha.28
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 +140 -58
- 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
|
|
121
157
|
|
|
122
|
-
|
|
123
|
-
|
|
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');
|
|
166
|
+
|
|
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,34 @@ 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} = be.config.login;
|
|
1179
|
+
const { schema } = be.config.db;
|
|
1180
|
+
|
|
1181
|
+
const hashPass = await generateScramVerifier(password);
|
|
1182
|
+
let params = [username, hashPass];
|
|
1183
|
+
let setters = [`${be.db.quoteIdent(passFieldName)} = $2`];
|
|
1184
|
+
if(passAlgorithmFieldName){
|
|
1185
|
+
setters.push(`${be.db.quoteIdent(passAlgorithmFieldName)} = $3`);
|
|
1186
|
+
params.push('PG-SHA256')
|
|
1187
|
+
}
|
|
1188
|
+
if(setUpdateDate && passUpdatedAtFieldName){
|
|
1189
|
+
setters.push(`${be.db.quoteIdent(passUpdatedAtFieldName)} = current_timestamp`)
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
const result = await client.query(`
|
|
1193
|
+
UPDATE ${(schema ? be.db.quoteIdent(schema) + '.' : '') + be.db.quoteIdent(table)}
|
|
1194
|
+
SET ${setters.join(', ')}
|
|
1195
|
+
WHERE ${be.db.quoteIdent(userFieldName)} = $1
|
|
1196
|
+
returning 1 as ok
|
|
1197
|
+
`, params
|
|
1198
|
+
).fetchOneRowIfExists();
|
|
1199
|
+
|
|
1200
|
+
if(result.rowCount === 0 && errorIfNoResult){
|
|
1201
|
+
throw Error ('no se encontró el usuario')
|
|
1202
|
+
};
|
|
1203
|
+
return result
|
|
1204
|
+
}
|
|
1122
1205
|
mainApp.loginPlusManager.setValidatorStrategy(
|
|
1123
1206
|
async function(req, username, password, done) {
|
|
1124
1207
|
try{
|
|
@@ -1152,36 +1235,42 @@ AppBackend.prototype.start = function start(opts){
|
|
|
1152
1235
|
done(null,false,{message:be.messages.unlogged.login.userOrPassFail});
|
|
1153
1236
|
return
|
|
1154
1237
|
}else{
|
|
1155
|
-
|
|
1238
|
+
let needsMigration = false;
|
|
1239
|
+
const user = data.row;
|
|
1240
|
+
if(!user[passFieldName]){
|
|
1241
|
+
done(null,false,{message:be.messages.unlogged.login.userOrPassFail});
|
|
1242
|
+
return
|
|
1243
|
+
}
|
|
1156
1244
|
const usaScramSha256 = user[passFieldName].startsWith('SCRAM-SHA-256$')
|
|
1157
1245
|
if (usaScramSha256){
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1246
|
+
let isScramValid = false;
|
|
1247
|
+
// 1. Intento con formato PG-Compatible (Nuevo)
|
|
1248
|
+
if(await verifyScramPG(password, user[passFieldName])){
|
|
1249
|
+
isScramValid = true;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// 2. Intento con formato Legacy (64 bytes)
|
|
1253
|
+
else if(await verifyScramLegacy(password, user[passFieldName])){
|
|
1254
|
+
isScramValid = true;
|
|
1255
|
+
needsMigration = true;
|
|
1256
|
+
}
|
|
1162
1257
|
if(!isScramValid){
|
|
1163
1258
|
done(null,false,{message:be.messages.unlogged.login.userOrPassFail});
|
|
1164
1259
|
return
|
|
1165
1260
|
}
|
|
1166
1261
|
}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.');
|
|
1262
|
+
if (md5(password+username.toLowerCase()) === user[passFieldName]){
|
|
1263
|
+
needsMigration = true;
|
|
1180
1264
|
}else{
|
|
1181
1265
|
done(null,false,{message:be.messages.unlogged.login.userOrPassFail});
|
|
1182
1266
|
return
|
|
1183
1267
|
}
|
|
1184
1268
|
}
|
|
1269
|
+
if(needsMigration){
|
|
1270
|
+
console.log('Ejecutando migración a SCRAM...');
|
|
1271
|
+
await updatePassword({client, username, password, setUpdateDate:false, errorIfNoResult:true});
|
|
1272
|
+
console.log('Migración completada.');
|
|
1273
|
+
}
|
|
1185
1274
|
}
|
|
1186
1275
|
//continua validando
|
|
1187
1276
|
if(data.rowCount==1){
|
|
@@ -1251,7 +1340,10 @@ AppBackend.prototype.start = function start(opts){
|
|
|
1251
1340
|
const storedHash = user[passFieldName];
|
|
1252
1341
|
if (oldPassword !== false) {
|
|
1253
1342
|
if (storedHash.startsWith('SCRAM-SHA-256$')) {//Intento SCRAM-SHA-256
|
|
1254
|
-
ok = await
|
|
1343
|
+
ok = await verifyScramPG(oldPassword, storedHash);
|
|
1344
|
+
if (!ok) {
|
|
1345
|
+
ok = await verifyScramLegacy(oldPassword, storedHash);
|
|
1346
|
+
}
|
|
1255
1347
|
} else { //Intento MD5
|
|
1256
1348
|
const md5Hash = md5(oldPassword + username.toLowerCase());
|
|
1257
1349
|
ok = (md5Hash === storedHash);
|
|
@@ -1266,17 +1358,7 @@ AppBackend.prototype.start = function start(opts){
|
|
|
1266
1358
|
}
|
|
1267
1359
|
// Si la verificación fue exitosa o se omitió (ok == true), generamos el nuevo hash SCRAM
|
|
1268
1360
|
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();
|
|
1361
|
+
const updateResult = await updatePassword({client, username, password:newPassword, setUpdateDate:true, errorIfNoResult:false});
|
|
1280
1362
|
ok = updateResult.rowCount === 1;
|
|
1281
1363
|
if (ok && be.config.login['double-dragon']) {
|
|
1282
1364
|
const doubleDragonSql = "ALTER USER " + be.db.quoteIdent(username) + " WITH PASSWORD " + be.db.quoteLiteral(newPassword);
|
package/package.json
CHANGED