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.
Files changed (2) hide show
  1. package/lib/backend-plus.js +139 -60
  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
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
- // Deriva la clave (Verifier)
123
- const verifierBuffer = await deriveKey(
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,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
- const user = data.row;
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
- const isScramValid = await verifyScram(
1159
- password,
1160
- user[passFieldName],
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
- 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.');
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 verifyScram(oldPassword, storedHash);
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
- // 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();
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
@@ -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.29",
5
5
  "author": "Codenautas <codenautas@googlegroups.com>",
6
6
  "license": "MIT",
7
7
  "repository": "codenautas/backend-plus",