Este artículo es el resultado del estudio de este tema, solicitado por el desarrollador Rubén.
Rubén tiene un desarrollo con el que quiere vender servicios a otras empresas y por las características de las instalaciones necesita «proteger» frente a posibles copias fraudulentas, su desarrollo.
Si consultáis en internet, veréis que se indica que como PHP no es un lenguaje compilado (es interpretado) no es posible protegerlo 100%, pero lo que sí se puede hacer es ponerlo un poco más difícil y eso es lo que hemos intentado.
El supuesto que hemos utilizado es el siguiente:
1.- La aplicación (PHP) y la base de datos (MySQL), se instala en una máquina Windows del Cliente. Además de la protección del desarrollo, tenemos que dotar al sistema de copias de seguridad ejecutadas por el Cliente (usuario no técnico) y de la capacidad de actualización de tablas de parametrización en las que se basa la solución.
2.- La aplicación se instala en un hosting de la empresa suministradora del software (servicio llave en mano), también se le da soporte de copias de seguridad/restauración, del aplicativo. A cada empresa cliente se le debe ofrecer un dominio de acceso diferenciado y sus datos no pueden estar accesibles ni compartidos con otros clientes. En los backup, sólo estarán los datos de su Empresa.
Con el supuesto (1), también otros desarrolladores me preguntaron como hacer una instalación local con un número concreto de días de evaluación y después de esos días, que el aplicativo no funcionara. Con esta solución, también se puede solucionar es requisito.
Solución Técnica
Para abordar este tema hemos trabajado en estas líneas de configuración:
- Tenemos que disponer de una única versión de aplicación de Producción para todas las instalaciones y hemos trabajado en disponer de un fichero de configuración que podamos actualizar sin tener que abrir el aplicativo en el IDE de desarrollo de PHPRunner. Solución de este artículo.
- Para la gestión y control de las instalaciones, necesitamos un Gestor de Licencias/Instalaciones, que además de los datos de personalización de la instalación, también disponga de la información administrativa de persona de contacto y fechas de inicio y finalización del contrato, para que el aplicativo consulte estos datos (de forma centralizada) y funcione o no, de acuerdo a ellos.
- Un solución de ofuscación del fichero de configuración de cara a poner todas las dificultades que podamos a los que deseen «copiar» nuestro software. Yo he utilizado, con resultado muy satisfactorio, esta solución: https://php-minify.com/php-obfuscator/
También dispone de solución para compresión (Minify) de PHP, que tiene menos impacto en los cambios de la programación que podría aplicarse a todo el código PHP, pero por el momento, hemos decidido no utilizarlo.
Resumiendo, una misma aplicación repartida en server remotos (instalación del Clientes) y server gestionado por la organización (servicio a múltiples empresas), pero con la misma aplicación.
Hemos realizado una prueba de concepto, para validar y comprobar que el sistema funciona y esto es lo que paso a explicaros.
La Gestión de Licencias es una única aplicación que gestiona todas las empresas y que gestiona principalmente esta información.
En la tabla (1) se organiza toda la información de la Empresa, la Administrativa y la Técnica.
Esta información se hace llegar a las instancias de las aplicaciones a través de la conexión de un servicio RestFull Api, haciendo el Gestor de Licencia de «Servidor de RestFull Api» y las aplicaciones de «Cliente de RestFull Api».
En el ejemplo, toda la información técnica de conexión a la Base de Datos se entrega a través de esta comunicación, no existiendo nada de la misma, en la aplicación.
También está la información de fecha de inicio y final del contrato, así como un check que permite bloquear temporalmente el aplicativo.
El campo ApiKey de la tabla (1) se va a utilizar para identificar a la Empresa. El objetivo es que la clave de identificación no sea deducible y por lo tanto más difícil de suplantar.
En la tabla (2), se almacenan todas las peticiones que le llegan al sistema, tanto si ha sido correcta y ha facilitado datos, como los fallos que se hayan podido producir.
Para el servidor de RestFull Api, he utilizado el framework SLIM 4.0, es muy sencillo y potente.
En la aplicación PHPRunner, se ha incluido este código en el directorio «restapi» y sus 3 principales ficheros son:
En este artículo explico cómo se construyen estos «server de RestFull Api».
La aplicación, que he llamado «business» es básica y sólo nos sirve para analizar y comprobar la capacidad de configuración a través de su fichero (MyCode/config.php):
<?php // Database connection || Conexión a la base de datos require_once __DIR__ . '/unirest_3.0.4/autoload.php'; require_once __DIR__.'/notifyError.php'; // Identification of the company || Identificación de la Empresa $key_business = '3eb4ad35fcceaafcf192a1a810593577'; // Control para verificar que se ha identificado en esta aplicación. Requerido por compartir identificación cookie de de sessión // Control to verify that it has been identified in this application. Required for sharing cookie of session if (isset($_SESSION['key_business'])) { if ($_SESSION['key_business'] <> $key_business ){ unset($_SESSION['key_business']); //********** Redirect to another page ************ header("Location: login.php?a=logout"); // User session is closed if not entry into this || Se cierra sesión de usuario si no se ha entrada en esta exit(); } } else { $response = Unirest\Request::post("http://localhost/license/restapi/v1/conexEmpresa/".$key_business, array("Authorization" => "3d524a53c110e4c22463b10ed32cef9d") ); $a = $response->code; // HTTP Status code $b = $response->headers; // Headers $c = $response->body; // Parsed body $d = $response->raw_body; // Unparsed body $f = json_decode($d,true); if ($a <> 200 || !is_array($f)){ NotifyError("Se ha producido error al conectar con el servidor de licencias. Error: ".$a." Message: ".$d); exit(); } if ($f['error']){ NotifyError("Se ha producido error al acceder al contrato de la Empresa. Error: ".$f['message']); exit(); } $_SESSION['business_data'] =$f['data']; $_SESSION['key_business'] = $key_business; } $host=$_SESSION['business_data']['DbHostConnection']; $user=$_SESSION['business_data']['DbUserConnection']; $pwd=$_SESSION['business_data']['DbPwdConnection']; $port=$_SESSION['business_data']['DbPortConnection']<> null?$f['data']['DbPortConnection']:3306; $sys_dbname=$_SESSION['business_data']['DbDbnameConnection']; // Database // Company customization variables || Variables de personalización de la Empresa $_SESSION['business']=$_SESSION['business_data']['corporateName']; $_SESSION["app-title"] = $_SESSION['business_data']['corporateName']; // Mandatory that they are session variables || Obligatorio que sean variables de sesión // All the variables that are required || Todas las variables que se requieram
Para realizar la peticiones al Server de Licencia se utiliza la biblioteca UNIREST, que por «debajo» hace peticiones CURL.
En resumen, en este fichero se indica el «ApiKey» de la Empresa y se establece la comunicación «conexEmpresa«. Si todo va bien, se recibe una estructura JSON con los datos de la table de esa Empresa. Cómo esta petición sólo se hace la primera vez que se conecta el usuario, los datos técnicos de acceso a la Base de Datos se cargan en variables de sesión para que en las próximas veces no haya que volver a pedir los datos. Cuando se hace «logout» en la aplicación, se destruye la sesión y la próxima vez que se conecte, vuelve a realizar la petición al servidor de licencia.
Para proteger esta información el fichero «config.php» se ofusca mediante esta URL https://php-minify.com/php-obfuscator/ y deja el fichero con este contenido:
<?php goto k8jbH; c5ljA: $host = $_SESSION["\x62\165\x73\151\156\x65\163\x73\137\144\x61\x74\141"]["\x44\142\110\x6f\163\x74\103\157\156\x6e\145\143\164\x69\x6f\x6e"]; goto FZDhA; jpP21: $port = $_SESSION["\142\x75\x73\151\156\x65\163\163\x5f\x64\141\x74\x61"]["\x44\x62\x50\x6f\x72\164\x43\x6f\x6e\x6e\145\143\164\151\157\156"] != null ? $f["\144\x61\x74\x61"]["\104\x62\120\157\x72\164\103\157\x6e\156\x65\x63\x74\151\x6f\156"] : 3306; goto t78bx; mhqke: $pwd = $_SESSION["\x62\165\163\x69\x6e\x65\163\163\137\144\x61\x74\141"]["\x44\142\120\x77\144\103\157\156\x6e\x65\143\x74\151\157\x6e"]; goto jpP21; jyVMZ: $_SESSION["\x62\165\163\151\x6e\145\163\x73"] = $_SESSION["\x62\x75\163\x69\x6e\145\163\x73\137\144\141\164\x61"]["\x63\157\162\160\x6f\162\141\164\x65\x4e\141\x6d\x65"]; goto cbKz1; k8jbH: require_once __DIR__ . "\57\165\x6e\151\x72\x65\x73\164\137\x33\x2e\x30\x2e\64\57\141\x75\164\x6f\154\157\141\144\x2e\x70\x68\160"; goto WgVrU; FZDhA: $user = $_SESSION["\142\x75\x73\151\x6e\145\163\163\137\x64\141\164\x61"]["\x44\142\x55\x73\145\162\x43\x6f\x6e\x6e\145\143\x74\x69\x6f\x6e"]; goto mhqke; t78bx: $sys_dbname = $_SESSION["\x62\165\163\x69\x6e\x65\163\163\137\144\141\164\x61"]["\104\x62\x44\x62\156\x61\x6d\145\x43\157\x6e\156\145\143\164\151\x6f\x6e"]; goto jyVMZ; WgVrU: require_once __DIR__ . "\57\x6e\x6f\x74\x69\146\x79\x45\162\x72\157\x72\56\160\x68\x70"; goto tFGAj; N8UnO: if (isset($_SESSION["\153\145\x79\x5f\142\x75\x73\x69\x6e\145\163\x73"])) { if ($_SESSION["\153\x65\x79\137\142\x75\x73\x69\156\145\163\163"] != $key_business) { unset($_SESSION["\x6b\145\x79\x5f\x62\x75\x73\151\156\145\163\163"]); header("\114\x6f\143\141\x74\x69\x6f\x6e\72\40\154\157\x67\151\x6e\x2e\x70\x68\x70\x3f\x61\x3d\x6c\157\147\157\x75\164"); die; } } else { $response = Unirest\Request::post("\150\164\164\x70\72\x2f\x2f\x6c\x6f\x63\141\154\150\x6f\x73\164\x2f\x6c\x69\143\145\156\163\x65\x2f\x72\145\x73\x74\x61\x70\151\x2f\x76\x31\57\143\157\x6e\145\170\105\x6d\160\162\x65\x73\x61\x2f" . $key_business, array("\x41\165\164\150\x6f\162\151\x7a\x61\164\x69\x6f\x6e" => "\63\144\x35\x32\x34\141\x35\x33\143\x31\x31\x30\145\64\143\x32\62\x34\66\63\x62\x31\60\x65\144\63\x32\143\x65\x66\71\x64")); $a = $response->code; $b = $response->headers; $c = $response->body; $d = $response->raw_body; $f = json_decode($d, true); if ($a != 200 || !is_array($f)) { NotifyError("\x53\145\x20\x68\141\40\x70\x72\157\x64\165\x63\x69\x64\x6f\x20\x65\162\x72\157\x72\40\141\x6c\x20\x63\x6f\x6e\145\143\x74\141\162\40\143\157\x6e\x20\145\x6c\x20\163\x65\x72\x76\151\144\x6f\162\40\x64\145\x20\154\151\143\x65\156\143\151\x61\163\x2e\x20\x45\x72\162\x6f\x72\72\40" . $a . "\x20\115\145\x73\x73\x61\x67\145\72\40" . $d); die; } if ($f["\145\x72\162\157\162"]) { NotifyError("\123\145\x20\x68\x61\x20\x70\162\x6f\144\165\x63\x69\144\x6f\40\x65\162\x72\157\x72\x20\141\154\40\141\143\x63\145\144\145\x72\40\x61\154\40\143\x6f\156\164\x72\141\x74\157\40\144\x65\x20\154\141\x20\105\x6d\160\162\145\x73\x61\x2e\40\105\162\162\157\x72\x3a\x20" . $f["\155\x65\x73\163\x61\147\x65"]); die; } $_SESSION["\x62\x75\x73\x69\156\145\x73\163\137\x64\141\164\x61"] = $f["\x64\x61\x74\x61"]; $_SESSION["\153\x65\x79\x5f\x62\165\163\151\x6e\145\x73\x73"] = $key_business; } goto c5ljA; tFGAj: $key_business = "\63\145\142\x34\x61\x64\x33\x35\146\143\143\145\141\x61\146\x63\146\61\x39\62\141\x31\x61\x38\61\x30\65\71\63\65\x37\67"; goto N8UnO; cbKz1: $_SESSION["\x61\x70\160\55\x74\151\164\154\145"] = $_SESSION["\142\x75\163\x69\156\x65\x73\163\137\x64\141\x74\x61"]["\143\157\162\x70\157\162\141\x74\145\x4e\141\155\x65"];
Si se produce algún problema, puede que la cookie de sesión se quede en un estado inconsistente y para facilitar el borrado de esta cookie y sesión he hecho este código, que se puede ejecutar directamente como «APP/clean«.
En la línea 3 se indica cuál es la clave de la cookie de sesión, que recordar está en el apartado de «Security» del PHPRunner.
Puede que sin probarlo, os resulte un poco complejo, pero creo que en cuanto lo descarguéis y probéis en vuestros PC’s, no vais a tener problema, no obstante, para cualquier duda, contactar conmigo a través de mi email y gustosamente os explicaré y ayudaré en todo.