Acabo de anunciar la nueva versión del plugin Android HTTP a ESC POS pero algunos usuarios todavía usan la versión anterior, por lo que es necesario generarles licencias cuando lo requieren.

En este post voy a documentar la forma en la que he logrado compilar el firmador de las licencias indicando las versiones y modo de uso para volver a esta documentación en el futuro cuando se me olvide.

Ya había hablado sobre las firmas digitales con Dart pero aquí hablaré sobre las versiones para que funcione, porque como bien sabes, hay nuevas versiones de Dart año con año.

Versión de Dart

El firmador es independiente del plugin, por lo que puede ejecutarse en cualquier lugar donde Dart sea compatible. Yo he probado en Android con Termux, Windows y en mi servidor Linux.

La versión que me ha funcionado es:

Dart SDK version: 2.12.0 (stable) (Thu Feb 25 19:50:53 2021 +0100) on “linux_x64”

Descargada de https://dart.dev/get-dart/archive con:

wget https://storage.googleapis.com/dart-archive/channels/stable/release/2.12.0/sdk/dartsdk-linux-x64-release.zip

La descomprimo con unzip dartsdk-linux-x64-release.zip.

Compilar firmador

Compilo con:

~/dart-sdk/bin/dart compile exe generar.dart

(No lo he agregado a la PATH. Eso es lo de menos)

Y el contenido de generar.dart es:

import 'dart:convert';
import 'dart:io';

import 'package:encrypt/encrypt.dart';
/*
 Puedes compilarme con
 dart compile exe generar_licencia_plugin_android.dart
 */

const String separador = "__";
const String separadorPlanoYFirmado = "###";
const ReemplazoDeSaltosDeLineaParaSerializarClave = "__";
const SaltoDeLinea = "\n";
void main(List<String> argumentos) async {
  if (argumentos.length != 4) {
    print("""Modo de uso:
nombre_ejecutable claveApi fechaInicio fechaFin clavePrivadaSerializada
Recuerda reemplazar los saltos de línea de la clave privada con $ReemplazoDeSaltosDeLineaParaSerializarClave""");
    return;
  }
  String claveApi = argumentos[0];
  String fechaInicio = argumentos[1];
  String fechaFin = argumentos[2];
  String clavePrivada = argumentos[3]
      .replaceAll(ReemplazoDeSaltosDeLineaParaSerializarClave, "\n");

  String licencia =
      generarLicencia(clavePrivada, claveApi, fechaInicio, fechaFin);
  stdout.write(licencia);
}

String generarLicencia(String clavePrivadaParaFirmarComoCadena, String claveApi,
    String fechaInicio, String fechaFin) {
  final clavePrivada = RSAKeyParser().parse(clavePrivadaParaFirmarComoCadena);
  final firmador =
      Signer(RSASigner(RSASignDigest.SHA256, privateKey: clavePrivada));
  // Primero calculamos la cadena original, que es concatenar clave api + fecha inicio + fecha fin
  String cuerpo = claveApi + separador + fechaInicio + separador + fechaFin;
  String mensajeFirmado = firmador.sign(cuerpo).base64;
  String licencia = cuerpo + separadorPlanoYFirmado + mensajeFirmado;
  return base64.encode(utf8.encode(licencia));
}

Por cierto, junto a ese archivo existe el pubspec.yaml cuyo contenido es el siguiente.

Ya ni recuerdo de dónde me lo copié desde el inicio para que su nombre sea ese que aparece XDD pero así lo dejaré por la precisión histórica. Lo importante es el environment y las dependencies:

name: newtify
description: >-
  Have you been turned into a newt?  Would you like to be?
  This package can help. It has all of the
  newt-transmogrification functionality you have been looking
  for.
version: 1.2.3
homepage: https://example-pet-store.com/newtify
documentation: https://example-pet-store.com/newtify/docs

environment:
  sdk: '>=2.10.0 <3.0.0'
dependencies:
  ecdsa: ^0.0.4
  encrypt: ^5.0.1
  intl: ^0.17.0
  

Para obtener las dependencias ejecutamos

dart pub get

Modo de uso

El modo de uso es el siguiente:

./generar.exe
Modo de uso:
nombre_ejecutable claveApi fechaInicio fechaFin clavePrivadaSerializada
Recuerda reemplazar los saltos de línea de la clave privada con __
  • claveApi el identificador del cliente
  • fechaInicio la fecha de inicio inclusiva de la licencia en formato YYYY-MM-DD
  • fechaFin la fecha fin inclusiva de la licencia en formato YYYY-MM-DD
  • clavePrivadaSerializada la clave privada RSA pero reemplazando saltos de línea con dos guiones bajos (__)

Y al ejecutarlo correctamente se va a imprimir la licencia lista para enviarla al cliente.

Solo para ver un ejemplo de uso que genera una licencia desde el 1 de enero de 2025 hasta el 31 de diciembre de 2025 lo haríamos así. Fíjate que he pasado la clave privada entre comillas y reemplazando saltos de línea con dos guiones bajos.

./generar.exe asd123 2025-01-01 2025-12-31 "-----BEGIN RSA PRIVATE KEY-----__MIIG5AIBAAKCAYEA6HOt6g675p5UW44l7xzdjBAJDp3WCvL5r637Zz05L/5yn22N__Lm+lXwf7GtJW74MFmK5Ddn+2qKTzWhwOwFsyi3oYlkYUA5cyDEWHrxUU7fihBihh__os+RlC2BxOOuEOOsImS9vbqlbnNyca78lbnuB9bQqpTbvzE5q6b5qpLwD9l77nDW__/Ej4IZAVS+blhHTJCOwjmqBB9w3DZD5BHrk0/r3Bmjq3qVIypy/5Pz+GMIGoLpSV__R5UCBGexwn3WclTI7cimPFh6WOnzIq5H/OyXDBy4ldyVPHc/OzdrC9batjk/Xm7y__rcpWNg9YnxZZLZKO66As8pZBroy0PGnvukD8pVCiNnUrlHJ/qz4b7sD9cJ4jJs5J__diTCNShFwKiLV36R9UQrNEXNBlUZTizdyKC0HxBgWVIPpnIKR8iWfL7MpW4kx4xL__pAniToRlXbDXYyc33BiAcMa1it8LU+XV5Zk/qqbgSpSmw6uIjE7atGM/LwGXwMcp__T1ap/qmmQYT4HXabAgMBAAECggGAJv95VwI/zfIULwQLIzGRjbUG//fE+DNJZorm__2aww0vd6XXrwq7C5atcY7qgJQ8eUgcgCs3e7ulFqLlz0sJrcQQAr7dI1+2A7Wkmz__+NCtoTsMMM9HihwOzXBRDCoygszfjSmfl5wfswcVVTNJVwlJgPEuMuAkedAVX9H0__owRh2BbhruApgUvwrRjSKdjD+tPpAmEzm/Z0+rDJbiDvpermlDJbr7dYJsUOkHQA__+fZ7Wdn20FHHNaovG3QWI44fEzFb7oMRFFcIixDQVSddshuZ1HnSzqsFNtpD3Tbe__DGyk0fK+QYzfv/X2rnS/e3dmFfgWvAsWJXW88UeXpNakqv/eI0VDF4enqURS+Qrp__apHKYTLNXFKwZM10VjQP48FGQmTDF2d3+ppjQzmibgCgTsbvzuEEr1vmMBRYQnSk__t7I7ZVoXG1KnB6gMxL26cmRoJh2Ta++Z2UmG8SZcAeqPZuY+zBrsuHNmoDea9N8w__3W2Rzine8N1tQiShkAZvlou7WOWRAoHBAPhfrzezkxAz7njFSlT84sF/R6+mR3mX__AXP5VcTPDwZtNX15SZA6CQjv+RqMMSIzXTfYdcsGvKPF2JZp0C2NuLEXrA8JODhd__aA+7hom7MclAVIYZ1CmmGJxdPZzJEAtGOrD7siHqZcadwnMOlErXdhWCVkGOId1O__z9ppv/Et21HFQbbAbdMLNv2Ohp26N807moWvYLjMb6l0v0jwhi/RQLAAxPiNN0YQ__+/U7+drY8ynMrTl2LAo9c4b4nb5tJDHhEwKBwQDvltexT4DD6wBzP3fzw1/RXRRQ__OZzbi4sRVhUDub6Kh9ZGrm4SpoWm3GlofG4AkHfx53gvCYnEqIWJZuJMrpUR4EpO__Ag2903lpgA7sG4sW46ZeNOVBvc23nGSmI6ejCwUwbsPbJM6cziRFDlqkSn9l2prn__/2esoF492fq+KXgxg9sX1WGRx1yHx/N4HU7B0Nl9XPOlXZIqB2LIwCXt2wx/oFYm__C0aFWgbuzURw/PBEW8v8rdEodmw2o1QRHSOOzVkCgcBwnVnh5J8SoqlGwxWP8VWT__LHXBc6BGxiwXfH7iDIwgetXc/WhYZ6f/EGefHN+ORUHH93J5SqWvkB5DHNkSPob2__bOhLrP97twZe08UNn/3T5UItx3pGJBRWmYQ9GYEXy5EC/CxpalEZPCCVcI/WW1kk__KYOYl6xNHtXpjzdDUGp36fuAtEFXhmc5ki0BSRTSDmtioAE36SHb8J5moQAGoFc3__NjIZDFZ6g2rqZ3ZDP0kJs877FdSkE/bUxNcpJ21Y3FcCgcEAi1NdxQelqsjbLynm__eIPSEa8eI/UM6YMBcShs+gim7GpHyjyfWAssR7d/OLq7QWrBxZPEiR3z1r/lP9zr__loojuAyFSU3abdwvi5FjnBv8S2hBFCGQfCWDHtY8lXzAfIjpEJwpGyQRXpBl+R56__yXVlFncEhAs7X+C2TSOYs8Lr2WeDHh4BKkQT5AX76IqqhP0NrOBD85Mxb21yHanX__pLUwVHuJ/X+rF9sdkvsdZNNCaY40VpFBw1TyYueh+H3Icr+pAoHBAKFT9AsytAP3__fJ+TRCDlGwXHmm37yVnLKBA3fDqX0KQVki581+OZtKgaRYHeTvupjw58KSj+OCvi__DocWcMwsrSboI88SFNXkbTP99/Enct/FE8MOH7OXr8dZfvjddoEcwRzcDpXhO5vh__J2aYR9uq9Cp4MHvJx+1g1N12lGBAceZE5d9oF1MKB9F/rhnt+vYMgY2mdH8A/JtM__DOOi9ghv8Tjn2pbhmEcXDSWe335Tmj/KwBSXT0Z8CYDlxRzp0W4hFg==__-----END RSA PRIVATE KEY-----"

Un poco de historia

Apenas hace unos días he estado creando un firmador con WASM y JS, ya que mi firmador anterior es un programa hecho en Go ejecutado a través de Termux y no me era fácil abrir los programas, iniciar el servidor y luego el navegador, así que decidí hacerlo en un nuevo firmador.

Actualmente manejo 3 tipos de licencias:

  • Licencia para el plugin Desktop. Manejada desde mi firmador WASM con JS
  • Licencia para plugin Android v1. Ese es el motivo de este post
  • Licencia para plugin Android v2. Manejada desde otro firmador propio en Kotlin

Quisiera manejar todo desde el firmador WASM, pero no es posible porque los otros 2 son dependientes del lenguaje Dart y Kotlin respectivamente.

Por ello es que todavía necesito el firmador de licencias del plugin versión 1, pero no quiero tenerlo como app independiente, así que lo quise colocar como una API.

Exponiendo firmador con servidor web

Después de pensarlo decidí usar mi servidor para generar las licencias de Android. Estaba pensando en programar el servidor un lenguaje de programación distinto a Dart, pero en Dart también se pueden crear servidores web así que hice uno y el código queda así:

import 'dart:convert';
import 'dart:io';

import 'package:encrypt/encrypt.dart';
/*
 Puedes compilarme con
 dart compile exe generar_licencia_plugin_android.dart
 */

const String separador = "__";
const String separadorPlanoYFirmado = "###";
const ReemplazoDeSaltosDeLineaParaSerializarClave = "__";
const SaltoDeLinea = "\n";

Future<void> main() async {
  final server = await HttpServer.bind(InternetAddress.anyIPv4, 8080);
  print('Servidor escuchando en puerto 8080...');
  const password = "123";

  await for (HttpRequest request in server) {
    if (request.method == 'POST') {
      try {
        final content = await utf8.decoder.bind(request).join();
        final data = jsonDecode(content);
        if (!(data is Map)) {
          request.response
            ..statusCode = HttpStatus.unauthorized
            ..write('JSON  inválido\n');

          await request.response.close();
          continue;
        }
        const requeridos = [
          "contraseña",
          "claveApi",
          "fechaInicio",
          "fechaFin",
          "clavePrivada"
        ];
        for (var requerido in requeridos) {
          if (!data.containsKey(requerido)) {
            request.response
              ..statusCode = HttpStatus.unauthorized
              ..write('No todas las claves requeridas están presentes\n');
            await request.response.close();
            continue;
          }
        }
        if (data["contraseña"] != password) {
          request.response
            ..statusCode = HttpStatus.unauthorized
            ..write('Contraseña incorrecta');
          await request.response.close();
          continue;
        }

        var licencia = generarLicencia(data["clavePrivada"], data["claveApi"],
            data["fechaInicio"], data["fechaFin"]);

        request.response
          ..statusCode = HttpStatus.ok
          ..write(licencia);
      } catch (e, stack) {
        print('Error: $e');
        print('Stacktrace:\n$stack');
        request.response
          ..statusCode = HttpStatus.badRequest
          ..write('Error\n');
      }
    } else {
      request.response
        ..statusCode = HttpStatus.methodNotAllowed
        ..write('Solo POST\n');
    }
    await request.response.close();
  }
}

String generarLicencia(String clavePrivadaParaFirmarComoCadena, String claveApi,
    String fechaInicio, String fechaFin) {
  final clavePrivada = RSAKeyParser().parse(clavePrivadaParaFirmarComoCadena);
  final firmador =
      Signer(RSASigner(RSASignDigest.SHA256, privateKey: clavePrivada));
  // Primero calculamos la cadena original, que es concatenar clave api + fecha inicio + fecha fin
  String cuerpo = claveApi + separador + fechaInicio + separador + fechaFin;
  String mensajeFirmado = firmador.sign(cuerpo).base64;
  String licencia = cuerpo + separadorPlanoYFirmado + mensajeFirmado;
  return base64.encode(utf8.encode(licencia));
}

La única protección que le he colocado es una contraseña. Así, si un atacante logra encontrar el endpoint tendría que adivinar la contraseña y obviamente necesita la clave RSA privada para obtener una licencia válida.

Si el post ha sido de tu agrado te invito a que me sigas para saber cuando haya escrito un nuevo post, haya actualizado algún sistema o publicado un nuevo software. Facebook | X | Instagram | Telegram | También estoy a tus órdenes para cualquier contratación en mi página de contacto