Google Apps Script & Verificador Criptográfico

Drive + Sheets + Anti-Fraude

Extracción en Gmail, respaldo automático de comprobantes en Google Drive / Google Sheets y validación criptográfica (SPF, DKIM, DMARC)

Seguridad Criptográfica & Respaldo Contable Automático en Google Drive

El script no solo valida en milisegundos que el pago sea real ante el POS; también crea de forma autónoma una carpeta en tu Google Drive con un Google Sheet donde archiva cada transacción con sus comprobantes criptográficos inmutables.

1. SPFIP Real

Sender Policy

Verifica que el correo provino de los servidores IP autorizados del banco y no de un atacante.

2. DKIMFirma RSA

Firma Criptográfica

Firma criptográfica del banco. Si se altera el monto o el nombre, la firma se quiebra al instante.

3. DMARCDirectiva

Domain Auth

Rechazo estricto según la política de seguridad del dominio bancario de Costa Rica.

4. TTL & Ref< 15 min

Anti-Reenvío Tardío

Valida el Date y el Message-ID para bloquear correos viejos reenviados a destiempo.

5. Drive & SheetAuto

Carpeta & Excel

Crea la carpeta "SinpePay - Registros" en Google Drive y agrega cada fila con las claves secretas.

Consola Interactiva de Pruebas en Vivo

Prueba en tiempo real la extracción bancaria, firmas digitales y simulación de la fila que se guarda en Google Sheets.

Haz clic en "Analizar y Validar Criptográficamente" o selecciona un ejemplo para ver el diagnóstico en tiempo real.

Código Fuente: Google Apps Script (.gs) con Google Drive y Sheets

Listo para copiar y pegar directamente en tu proyecto de Google Apps Script.

Comercio Asignado:
ID inyectado automáticamente en el código
/**
 * ============================================================================
 * SINPEPAY - GOOGLE APPS SCRIPT: EXTRACTOR, AUDITORÍA EN DRIVE & CRIPTOGRAFÍA
 * ============================================================================
 * 1. Extrae: Monto, Número de Referencia, Banco, Ordenante (quien pagó), Fecha/Hora.
 * 2. Extrae Claves Secretas: SPF, DKIM, DMARC, Message-ID y Firma Digital.
 * 3. Crea automáticamente en Google Drive una Carpeta y un Google Sheet con
 *    el registro contable y las pruebas criptográficas de cada transacción.
 * 4. Envía por Webhook seguro a SinpePay para confirmar el cobro en el POS.
 * 
 * Configuración:
 * Generado automáticamente por SinpePay para tu comercio.
 * 1. Pega este código en https://script.google.com
 * 2. Ejecuta una vez para autorizar y crea el activador cada 1 minuto.
 * ============================================================================
 */

const SINPEPAY_CONFIG = {
  WEBHOOK_URL: 'https://sinpepay.pages.dev/api/transactions',
  COMERCIO_ID: '${comercioId}', // Inyectado automáticamente por SinpePay para tu negocio
  SUCURSAL_ID: '${sucursalId}',
  MAX_MINUTOS_VENTANA: 15, // Rechaza correos con fecha original de más de 15 minutos
  DRIVE_CARPETA_NOMBRE: 'SinpePay - Registros Bancarios',
  SHEET_NOMBRE: 'Auditoría y Respaldos SINPE Móvil'
};

/**
 * 1. Función de Configuración Inicial: Crea de inmediato la carpeta y el Sheet en Google Drive
 * Puedes seleccionar esta función y darle "Ejecutar" para verificar que se cree en Drive.
 */
function crearCarpetaYHojaEnDrive() {
  const sheet = obtenerOCrearHojaAuditoria();
  if (sheet) {
    Logger.log('✅ Carpeta y Google Sheet creados con éxito en tu Google Drive.');
  }
}

/**
 * 2. Función principal: Procesa notificaciones bancarias de Costa Rica
 * Puede ejecutarse por Reloj (cada 1 min) o Bajo Demanda desde el Datáfono Virtual.
 * @param {number|null} montoBuscado - Si el datáfono busca un monto exacto en tiempo real
 */
function procesarNotificacionesSinpe(montoBuscado) {
  // Siempre asegura que la carpeta y el Google Sheet existan primero
  const sheetAuditoria = obtenerOCrearHojaAuditoria();
  let transaccionEncontrada = null;

  // Directorio bancario completo de Costa Rica (Bancos Estatales, Privados, Cooperativas y Mutuales)
  const query = 'from:(@baccredomatic.com OR @bac.cr OR @bncr.fi.cr OR @bancobcr.com OR @bancopopular.fi.cr OR @bp.fi.cr OR @davivienda.cr OR @promerica.fi.cr OR @scotiabank.com OR @scotiabankcr.com OR @lafise.com OR @bct.cr OR @bancocathay.com OR @coopealianza.fi.cr OR @coopeande.com OR @coopenae.fi.cr OR @holawink.com OR @mucap.fi.cr OR @grupomutual.fi.cr OR @coopecaja.fi.cr) is:unread';
  const threads = GmailApp.search(query, 0, 15);
  
  Logger.log('Hilos bancarios encontrados: ' + threads.length);
  if (threads.length === 0) {
    Logger.log('Buzón al día: No hay correos bancarios pendientes por procesar.');
    return { encontrado: false, procesados: 0, transaccion: null };
  }

  let totalProcesados = 0;
  for (let i = 0; i < threads.length; i++) {
    const messages = threads[i].getMessages();
    for (let j = 0; j < messages.length; j++) {
      const msg = messages[j];
      if (!msg.isUnread()) continue;
      
      try {
        const rawContent = msg.getRawContent(); // MIME completo con encabezados RFC 822
        const body = msg.getPlainBody() || msg.getBody();
        const subject = msg.getSubject();
        const fromAddress = msg.getFrom();
        const msgDate = msg.getDate();
        
        // 1. Extracción y Verificación de Encabezados Criptográficos (SPF, DKIM, DMARC)
        const cryptoAuth = analizarEncabezadosCriptograficos(rawContent, msgDate);
        
        // 2. Extracción de Datos Bancarios y Claves Digitales del Recibo
        const datos = extraerDatosBancarios(body, subject, fromAddress);
        
        // 3. Validación Anti-Reenvío (Anti-Replay)
        if (cryptoAuth.minutosTranscurridos > SINPEPAY_CONFIG.MAX_MINUTOS_VENTANA) {
          Logger.warn('ALERTA: Posible reenvío tardío (>15 min). Minutos: ' + cryptoAuth.minutosTranscurridos);
        }
        
        // 4. Guardar copia inmutable en Google Sheets (Drive)
        if (datos.monto && datos.numeroReferencia) {
          const coincideMonto = !montoBuscado || Math.abs(datos.monto - montoBuscado) < 1;

          registrarEnGoogleSheet(sheetAuditoria, datos, cryptoAuth, msgDate, subject);

          // 5. Preparar Payload y enviar a SinpePay para activar el Datáfono
          const payload = {
            comercio_id: SINPEPAY_CONFIG.COMERCIO_ID,
            sucursal_id: SINPEPAY_CONFIG.SUCURSAL_ID,
            monto: datos.monto,
            numero_referencia: datos.numeroReferencia,
            banco_emisor: datos.banco,
            correo_remitente: fromAddress,
            nombre_ordenante: datos.ordenante,
            telefono_origen: datos.telefonoOrigen,
            telefono_destino: datos.telefonoDestino,
            fecha_banco: msgDate.toISOString(),
            autenticado: cryptoAuth.autenticado ? 1 : 0,
            seguridad: {
              spf: cryptoAuth.spf,
              dkim: cryptoAuth.dkim,
              dmarc: cryptoAuth.dmarc,
              message_id: cryptoAuth.messageId,
              clave_digital: datos.claveSeguridad,
              minutos_transcurridos: cryptoAuth.minutosTranscurridos,
              posible_reenvio_fraudulento: cryptoAuth.posibleReenvio
            },
            raw_email_body: body.substring(0, 1500)
          };
          
          enviarWebhookSinpePay(payload);
          msg.markRead(); // Marcar como procesado solo tras registro
          totalProcesados++;

          if (coincideMonto && !transaccionEncontrada) {
            transaccionEncontrada = datos;
          }
        } else {
          Logger.warn('No se pudo extraer monto o referencia del mensaje ID: ' + msg.getId());
        }
      } catch (err) {
        Logger.error('Error procesando mensaje: ' + err.toString());
      }
    }
  }

  return {
    encontrado: transaccionEncontrada !== null,
    transaccion: transaccionEncontrada,
    procesados: totalProcesados
  };
}

/**
 * 3. DESPERTADOR BAJO DEMANDA (WEB APP PARA DATÁFONO VIRTUAL)
 * Permite que el Datáfono Virtual despierte este script INMEDIATAMENTE
 * cuando el dependiente digita el monto y presiona ENTER (ciclos de 15s).
 * Cero consumo de cuota de Google cuando la tienda está vacía.
 */
function doGet(e) {
  return responderDatafonoBajoDemanda(e);
}

function doPost(e) {
  return responderDatafonoBajoDemanda(e);
}

function responderDatafonoBajoDemanda(e) {
  try {
    const params = (e && e.parameter) || {};
    const montoBuscado = params.monto ? parseFloat(params.monto) : null;
    const res = procesarNotificacionesSinpe(montoBuscado);

    const respuesta = {
      status: res.encontrado ? 'aprobado' : 'no_encontrado',
      monto_buscado: montoBuscado,
      encontrado: res.encontrado,
      transaccion: res.transaccion,
      procesados: res.procesados,
      timestamp: new Date().toISOString()
    };

    return ContentService.createTextOutput(JSON.stringify(respuesta))
      .setMimeType(ContentService.MimeType.JSON);
  } catch (err) {
    return ContentService.createTextOutput(JSON.stringify({
      status: 'error',
      mensaje: err.toString()
    })).setMimeType(ContentService.MimeType.JSON);
  }
}

/**
 * Obtiene o crea la carpeta en Google Drive y la hoja de Google Sheets con columnas formateadas
 */
function obtenerOCrearHojaAuditoria() {
  try {
    let carpeta;
    const carpetas = DriveApp.getFoldersByName(SINPEPAY_CONFIG.DRIVE_CARPETA_NOMBRE);
    if (carpetas.hasNext()) {
      carpeta = carpetas.next();
    } else {
      carpeta = DriveApp.createFolder(SINPEPAY_CONFIG.DRIVE_CARPETA_NOMBRE);
      Logger.log('Carpeta creada en Google Drive: ' + SINPEPAY_CONFIG.DRIVE_CARPETA_NOMBRE);
    }

    let spreadsheet;
    const archivos = carpeta.getFilesByName(SINPEPAY_CONFIG.SHEET_NOMBRE);
    if (archivos.hasNext()) {
      const archivo = archivos.next();
      spreadsheet = SpreadsheetApp.open(archivo);
    } else {
      spreadsheet = SpreadsheetApp.create(SINPEPAY_CONFIG.SHEET_NOMBRE);
      const fileId = spreadsheet.getId();
      const tempFile = DriveApp.getFileById(fileId);
      try {
        tempFile.moveTo(carpeta);
      } catch (errMove) {
        try { carpeta.addFile(tempFile); } catch (e2) {}
      }

      const sheet = spreadsheet.getActiveSheet();
      sheet.setName('Auditoría SINPE');
      const encabezados = [
        'Fecha Registro',
        'Fecha Banco',
        'Banco Emisor',
        'N° Referencia / Comprobante',
        'Monto (₡)',
        'Persona que Pagó (Ordenante)',
        'Detalle / Asunto',
        'SPF',
        'DKIM',
        'DMARC',
        'Message-ID (RFC 822)',
        'Clave Digital / Firma Electrónica',
        'Estado Verificación'
      ];
      sheet.appendRow(encabezados);

      // Estilo visual moderno para la cabecera del Sheet
      const headerRange = sheet.getRange(1, 1, 1, encabezados.length);
      headerRange.setFontWeight('bold');
      headerRange.setBackground('#064e3b');
      headerRange.setFontColor('#ecfdf5');
      sheet.setFrozenRows(1);
      Logger.log('Google Sheet de auditoría creado y configurado exitosamente.');
    }

    return spreadsheet.getSheetByName('Auditoría SINPE') || spreadsheet.getActiveSheet();
  } catch (e) {
    Logger.error('Error al inicializar Drive/Sheets: ' + e.toString());
    return null;
  }
}

/**
 * Agrega una fila con todos los datos comerciales y criptográficos en Google Sheets
 */
function registrarEnGoogleSheet(sheet, datos, cryptoAuth, msgDate, subject) {
  if (!sheet) return;
  try {
    const estadoTexto = cryptoAuth.autenticado
      ? 'APROBADO (100% Auténtico)'
      : (cryptoAuth.posibleReenvio ? 'ALERTA (Reenvío Tardío >15m)' : 'RECHAZADO (Firma Inválida)');

    sheet.appendRow([
      new Date().toLocaleString('es-CR'),
      msgDate ? msgDate.toLocaleString('es-CR') : 'N/A',
      datos.banco,
      datos.numeroReferencia,
      datos.monto,
      datos.ordenante,
      subject || 'Transferencia SINPE',
      cryptoAuth.spf,
      cryptoAuth.dkim,
      cryptoAuth.dmarc,
      cryptoAuth.messageId,
      datos.claveSeguridad,
      estadoTexto
    ]);
  } catch (e) {
    Logger.error('Error insertando fila en Google Sheets: ' + e.toString());
  }
}

/**
 * Analiza los encabezados RFC 822 del correo para validar SPF, DKIM, DMARC y marcas de tiempo
 */
function analizarEncabezadosCriptograficos(rawContent, msgDate) {
  const authResults = extraerHeader(rawContent, 'Authentication-Results');
  const receivedSpf = extraerHeader(rawContent, 'Received-SPF');
  const dkimSig = extraerHeader(rawContent, 'DKIM-Signature');
  const messageId = extraerHeader(rawContent, 'Message-ID');
  
  // Validaciones Criptográficas
  const spfPass = /spf=pass/i.test(authResults) || /^pass/i.test(receivedSpf);
  const dkimPass = /dkim=pass/i.test(authResults) || dkimSig.length > 10;
  const dmarcPass = /dmarc=pass/i.test(authResults);
  
  // Cálculo de Tiempo Transcurrido (Anti-Replay)
  const ahora = new Date().getTime();
  const fechaCorreo = msgDate ? msgDate.getTime() : ahora;
  const minutosTranscurridos = Math.max(0, Math.round((ahora - fechaCorreo) / (1000 * 60)));
  const posibleReenvio = minutosTranscurridos > SINPEPAY_CONFIG.MAX_MINUTOS_VENTANA;
  
  return {
    spf: spfPass ? 'PASS' : 'FAIL',
    dkim: dkimPass ? 'PASS' : 'FAIL',
    dmarc: dmarcPass ? 'PASS' : 'FAIL',
    autenticado: spfPass && dkimPass && !posibleReenvio,
    messageId: messageId || 'DESCONOCIDO',
    minutosTranscurridos: minutosTranscurridos,
    posibleReenvio: posibleReenvio
  };
}

/**
 * Extrae valores de encabezados RFC 822
 */
function extraerHeader(rawContent, headerName) {
  const regex = new RegExp('^' + headerName + ':\\s*([^\\r\\n]+(?:\\r?\\n[ \\t]+[^\\r\\n]+)*)', 'mi');
  const match = rawContent.match(regex);
  return match ? match[1].replace(/\r?\n[ \t]+/g, ' ').trim() : '';
}

/**
 * Motor Regex de Extracción Bancaria para Bancos de Costa Rica
 */
function extraerDatosBancarios(cuerpo, asunto, from) {
  const texto = (asunto + ' ' + cuerpo).replace(/\r?\n|\r/g, ' ');
  let banco = 'Desconocido';
  
  // 1. Identificar Banco (Bancos Estatales, Privados, Cooperativas y Mutuales de CR)
  if (/baccredomatic|bac\.cr|credomatic/i.test(from) || /bac|credomatic/i.test(texto)) {
    banco = 'BAC Credomatic';
  } else if (/bancobcr\.com|bcr/i.test(from) || /banco de costa rica|\bbcr\b/i.test(texto)) {
    banco = 'Banco de Costa Rica (BCR)';
  } else if (/bncr\.fi\.cr|bncr/i.test(from) || /banco nacional|\bbncr\b/i.test(texto)) {
    banco = 'Banco Nacional (BNCR)';
  } else if (/bancopopular|bp\.fi\.cr/i.test(from) || /banco popular|\bbp\b/i.test(texto)) {
    banco = 'Banco Popular';
  } else if (/davivienda/i.test(from) || /davivienda/i.test(texto)) {
    banco = 'Davivienda';
  } else if (/promerica/i.test(from) || /promerica/i.test(texto)) {
    banco = 'Banco Promerica';
  } else if (/scotiabank/i.test(from) || /scotiabank/i.test(texto)) {
    banco = 'Scotiabank';
  } else if (/lafise/i.test(from) || /lafise/i.test(texto)) {
    banco = 'Banco Lafise';
  } else if (/bct\.cr/i.test(from) || /\bbct\b|banco bct/i.test(texto)) {
    banco = 'Banco BCT';
  } else if (/cathay/i.test(from) || /cathay/i.test(texto)) {
    banco = 'Banco Cathay';
  } else if (/coopealianza/i.test(from) || /coopealianza/i.test(texto)) {
    banco = 'Coopealianza';
  } else if (/coopeande/i.test(from) || /coope\s*ande/i.test(texto)) {
    banco = 'Coope Ande';
  } else if (/coopenae/i.test(from) || /coopenae/i.test(texto)) {
    banco = 'Coopenae';
  } else if (/holawink|wink/i.test(from) || /wink/i.test(texto)) {
    banco = 'Wink';
  } else if (/mucap/i.test(from) || /mucap/i.test(texto)) {
    banco = 'MUCAP';
  } else if (/grupomutual/i.test(from) || /grupo\s*mutual/i.test(texto)) {
    banco = 'Grupo Mutual';
  } else if (/coopecaja/i.test(from) || /coopecaja/i.test(texto)) {
    banco = 'Coopecaja';
  }
  
  // 2. Extraer Monto
  let monto = null;
  const matchMonto = texto.match(/(?:₡|CRC|monto:?|valor:?)\s*([0-9]{1,3}(?:[.,][0-9]{3})+|[0-9]+(?:\.[0-9]{2})?|[0-9]+)/i);
  if (matchMonto && matchMonto[1]) {
    let numStr = matchMonto[1].replace(/,/g, '');
    monto = parseFloat(numStr);
  }
  
  // 3. Extraer Número de Referencia / Comprobante
  let numeroReferencia = null;
  const matchRef = texto.match(/(?:referencia|ref|comprobante|num(?:ero)?\.?\s*ref(?:erencia)?)\s*[:#]?\s*([A-Za-z0-9_-]{6,30})/i);
  if (matchRef && matchRef[1]) {
    numeroReferencia = matchRef[1].trim();
  } else {
    const matchNumerico = texto.match(/\b\d{8,16}\b/);
    if (matchNumerico) numeroReferencia = matchNumerico[0];
  }
  
  // 4. Extraer Nombre de la Persona que Transfirió (Ordenante)
  let ordenante = 'No especificado';
  const matchOrdenante = texto.match(/(?:de|ordenante|cliente|remitente|transferido por|de parte de)\s*:\s*([A-Za-zÁÉÍÓÚáéíóúÑñ ]{4,40})(?:\s{2,}|\.|,|cuenta|monto|$)/i);
  if (matchOrdenante && matchOrdenante[1]) {
    ordenante = matchOrdenante[1].trim();
  }
  
  // 5. Extraer Clave de Seguridad / Firma Electrónica Bancaria
  let claveSeguridad = 'N/A';
  const matchFirma = texto.match(/(?:firma\s*(?:digital|electr[oó]nica)|clave\s*(?:digital|de\s*seguridad)|c[oó]digo\s*de\s*seguridad)\s*[:#]?\s*([A-Za-z0-9_-]{6,35})/i);
  if (matchFirma && matchFirma[1]) {
    claveSeguridad = matchFirma[1].trim();
  }
  
  // 6. Extraer Teléfono de Quien Pagó (Remitente / Origen)
  let telefonoOrigen = 'No especificado';
  const matchTelOrigen = texto.match(/(?:tel[eé]fono\s*(?:de\s*origen|origen|remitente|pagador)?|desde\s*el\s*tel(?:[eé]fono)?|origen)\s*[:#]?\s*([5-8][0-9]{3}[-\s]?[0-9]{4})/i);
  if (matchTelOrigen && matchTelOrigen[1]) {
    telefonoOrigen = matchTelOrigen[1].replace(/\s|-/g, '');
  }

  // 7. Extraer Teléfono del Negocio Receptor (Destino SINPE)
  let telefonoDestino = 'No especificado';
  const matchTelDestino = texto.match(/(?:al\s*tel[eé]fono|tel[eé]fono\s*destino|acreditado\s*al\s*(?:tel(?:[eé]fono)?)?|destino)\s*[:#]?\s*([5-8][0-9]{3}[-\s]?[0-9]{4})/i);
  if (matchTelDestino && matchTelDestino[1]) {
    telefonoDestino = matchTelDestino[1].replace(/\s|-/g, '');
  }

  return {
    banco: banco,
    monto: monto,
    numeroReferencia: numeroReferencia,
    ordenante: ordenante,
    telefonoOrigen: telefonoOrigen,
    telefonoDestino: telefonoDestino,
    claveSeguridad: claveSeguridad
  };
}

/**
 * Envía la transacción validada al endpoint webhook de SinpePay
 */
function enviarWebhookSinpePay(payload) {
  const options = {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  };
  
  const response = UrlFetchApp.fetch(SINPEPAY_CONFIG.WEBHOOK_URL, options);
  Logger.log('Respuesta de SinpePay: ' + response.getContentText());
}

Guía Rápida de Instalación en tu Cuenta de Google (2 Minutos)

1

Crear Proyecto en Google

Ingresa a script.google.com con tu cuenta de Google y haz clic en el botón "Nuevo proyecto".

2

Pegar y Autorizar Servicios

Borra el código que trae por defecto, pega el script copiado arriba y haz clic en Guardar (💾). Haz clic en Ejecutar para conceder permisos a Gmail, Google Drive y Google Sheets una sola vez.

3

Activar Temporizador

En el menú lateral izquierdo haz clic en Activadores (icono de reloj) ➔ Añadir activador ➔ Selecciona Basado en tiempo cada 1 minuto. ¡El script creará tu carpeta en Drive y registrará cada venta automáticamente!