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.
Files changed (2) hide show
  1. package/lib/backend-plus.js +140 -58
  2. package/package.json +1 -1
@@ -85,61 +85,115 @@ function md5(text){
85
85
 
86
86
  const DEFAULT_ITERATIONS = 4096;
87
87
  const HASH_ALGORITHM = 'sha256';
88
- const KEY_LENGTH = 64; // Longitud de la clave (64 bytes)
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, salt, iterations) {
101
+ function deriveKey(password, saltBuffer, iterations, keyLength) {
96
102
  return new Promise((resolve, reject) => {
97
103
  crypto.pbkdf2(
98
104
  password,
99
- salt,
105
+ saltBuffer,
100
106
  iterations,
101
- KEY_LENGTH,
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
- * Genera la cadena de verificación SCRAM-SHA-256 en formato PostgreSQL.
113
- * Formato: SCRAM-SHA-256$iterations:salt$verifier
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
- * @returns {Promise<string>} La cadena SCRAM completa.
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 generateScramVerifier(password) {
118
- // Genera Salt aleatoria
119
- const saltBuffer = crypto.randomBytes(32);
120
- const saltBase64 = bufferToBase64(saltBuffer);
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
- // Deriva la clave (Verifier)
123
- const verifierBuffer = await deriveKey(
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
- saltBase64,
126
- DEFAULT_ITERATIONS
169
+ saltBuffer,
170
+ iterations,
171
+ SCRAM_KEY_LENGTH
127
172
  );
128
- const verifierBase64 = bufferToBase64(verifierBuffer);
129
173
 
130
- // Ensambla la cadena final (PostgreSQL style)
131
- // Usa $ como delimitador principal (aunque PostgreSQL lo pone dentro de la tabla).
132
- // El formato canónico es 'SCRAM-SHA-256$i:salt, verifier$encoded_verifier'
133
- return `SCRAM-SHA-256$${DEFAULT_ITERATIONS}:${saltBase64}$${verifierBase64}`;
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 en texto plano contra la cadena SCRAM almacenada.
138
- * @param {string} password - Contraseña en texto plano ingresada por el usuario.
139
- * @param {string} storedScramString - Cadena SCRAM completa (ej: SCRAM-SHA-256$4096:SALT_BASE64$VERIFIER_BASE64).
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 verifyScram(password, storedScramString) {
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
- const user = data.row;
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
- const isScramValid = await verifyScram(
1159
- password,
1160
- user[passFieldName],
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
- console.log('Autenticación MD5 OK. Ejecutando migración a SCRAM...');
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 verifyScram(oldPassword, storedHash);
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
- // hash en ormato completo de PostgreSQL: SCRAM-SHA-256$i:salt$verifier
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "backend-plus",
3
3
  "description": "Backend for the anti Pareto rule",
4
- "version": "2.5.2-betha.27",
4
+ "version": "2.5.2-betha.28",
5
5
  "author": "Codenautas <codenautas@googlegroups.com>",
6
6
  "license": "MIT",
7
7
  "repository": "codenautas/backend-plus",