2014-09-03

Convertir String Hexadecimal en array de bytes en Java

Os presento el método inverso al de Convertir array de bytes en String de los valores en Hexadecimal

El método más optimizado que he encontrado en Java es el siguiente:
byte[] javax.xml.bind.DatatypeConverter.parseHexBinary(String lexicalXSDHexBinary)
Para mostrar su eficiencia voy a compararlo con este otro método [stackoverflow.com] bastante bien valorado por los usuarios:
    private static byte[] hexStringToByteArray(String hex) {
        int len = hex.length();
        byte[] data = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
                    + Character.digit(hex.charAt(i + 1), 16));
        }
        return data;
    }
Y para ello, aprovecho para mostrar como se puede hacer un pequeño benchmark comparativo:
import javax.xml.bind.DatatypeConverter;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;

/**
 * @author Raul
 */
public final class Benchmark {

    private static byte[] hexStringToByteArray(String hex) {
        int len = hex.length();
        byte[] data = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
                    + Character.digit(hex.charAt(i + 1), 16));
        }
        return data;
    }

    private static final char[] VALORES = "0123456789ABCDEF".toCharArray();
    private static final int MIN = 0;
    private static final int MAX = 15;

    private static final int NUM_TESTS = 1000000;
    private static final int LENGTH_HEX = 64;

    private static final long TIME_ACCURACY = 1000000;

    public static void main(String[] args) {
        // Preparamos los tests
        Random random = new Random();
        int range = MAX - MIN + 1;
        List<String> tests = new LinkedList<String>();
        for (int i = 0; i < NUM_TESTS; i++) {
            StringBuilder sb = new StringBuilder(LENGTH_HEX);
            for (int b = 0; b < LENGTH_HEX; b++) {
                int randomPos = random.nextInt(range) + MIN;
                sb.append(VALORES[randomPos]);
            }
            tests.add(sb.toString());
        }

        // Iniciamos
        long startTime = 0, endTime = 0;
        System.gc();

        // DatatypeConverter
        startTime = System.nanoTime();
        for (String test : tests) {
            byte[] result = DatatypeConverter.parseHexBinary(test);
        }
        endTime = System.nanoTime();
        System.out.printf("%22s %10d%n", "DatatypeConverter:",
                (endTime - startTime) / TIME_ACCURACY);
        System.gc();

        // hexStringToByteArray
        startTime = System.nanoTime();
        for (String test : tests) {
            byte[] result = hexStringToByteArray(test);
        }
        endTime = System.nanoTime();
        System.out.printf("%22s %10d%n", "hexStringToByteArray:",
                (endTime - startTime) / TIME_ACCURACY);
        System.gc();
    }

}
Resultado:
    DatatypeConverter:        663
 hexStringToByteArray:        908

El paquete de DatatypeConverter no está disponible en Android, así que los que necesitéis este método podéis utilizar el hexStringToByteArray expuesto aquí, o el código del parseHexBinary extraído del propio paquete y que podéis encontrar en http://stackoverflow.com/a/11139098

2014-08-26

Convertir array de bytes en String de los valores en Hexadecimal

Cuando trabajaba en el algoritmo para crear un MAC ANSI X9.19 tuve que convertir el resultado que estaba en un array de bytes (byte[] result) en un String que representase cada byte como un valor Hexadecimal. Para ello, utilicé una función que encontré en stackoverflow y que resultaba elegante a simple vista:
    private static String toHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder(bytes.length * 2);
        for (byte b : bytes) {
            sb.append(String.format("%02x", b & 0xff)); // "%02X" for uppercase
        }
        return sb.toString();
    }

El caso es que mientras buscaba el código para el MAC ANSI X9.19 me encontré muchas formas de implementar esta conversión y he tenido la curiosidad de indagar cuál es la más eficiente.

Quiero comentar que hay librerías de uso bastante común que incorporan esta conversión, por ejemplo:
  • org.apache.commons.codec.binary.Hex.encodeHexString() de la librería Apache Commons Codec
  • org.bouncycastle.util.encoders.Hex.encode() de la librería Bouncy Castle
Pero es más, aunque mucha gente no lo sabe y acaba recurriendo a librerías externas, viene también incluida en la propia distribución de Java: javax.xml.bind.DatatypeConverter.printHexBinary()

Pues bien, ninguna de las implementaciones comentadas resulta ser demasiado eficiente. Así que si necesitáis una conversión que sea rápida, no queréis incluir librerías externas o estáis programando en Android y necesitáis esta conversión porque se dispone del DatatypeConverter en Android, aquí va el método óptimo de conversión. Mediante un benchmark personalizado que construí he podido comprobar que es el doble de rápido que el mejor de los demás que he probado y 100 veces más rápido que las conversiones incluidas en librerías comentadas o la propia DatatypeConverter.printHexBinary()

    private static final char[] hexArray = "0123456789ABCDEF".toCharArray();

    /**
     * Encode the input bytes producing a Hexadecimal output string
     *
     * @param bytes
     * @return Hexadecimal representation of the input bytes
     */
    public static final String byteArrayToHexString(final byte[] bytes) {
        char[] hexChars = new char[bytes.length * 2];
        for (int j = 0; j < bytes.length; j++) {
            int v = bytes[j] & 0xFF;
            hexChars[j * 2] = hexArray[v >>> 4];
            hexChars[j * 2 + 1] = hexArray[v & 0x0F];
        }
        return new String(hexChars);
    }

Todo el mérito y mi reconocimiento es para esta contribución en stackoverflow: http://stackoverflow.com/a/9855338

Cifrado MAC ANSI X9.19 / ISO 9807 / ISO/IEC 9797 ...

Cuando me pidieron un cifrado MAC para firmar un tipo de transaciones bancarias no imaginé cuánto tiempo me llevaría dar con la codificación y la poca información directa que iba a poder encontrar, así que he decidido publicar aquí el resultado de todas mis averiguaciones y aportar una implementación en Java sin necesidad de librerías externas.

Empecemos, un MAC ("Message Authentication Code") es un código que permite validar la autenticidad de un mensaje, por lo tanto, sirve como firma electrónica del mensaje, aunque se difiere de ésta en que un MAC es calculado y verificado con la misma clave, por lo que sólo puede ser verificado por el receptor previsto. Se obtiene aplicando un cifrado con una clave secreta a un mensaje. El resultado lo conocemos también como "checksum".

Como se trataba de entorno bancario, existe un estandar de la banca nacional de EEUU para autenticación de transacciones bancarias que es el ANSI X9.19 (que es una actualización del X9.9, para quién tenga más interés). El algoritmo definido por ANSI X9.19 es conocido como Retail-MAC y es un cifrado de los llamados DES-CBC MAC, es decir cifrado DES ("Data Encryption Standard") en modo CBC ("Cipher Block Chaining"). Existe una norma internacional equivalente que es la ISO 9807 y que es un poco más abierta en cuanto a que permite otros cifrados para obtener el MAC. El cálculo de un MAC como se describe en ANSI X9.19 e ISO 9807 es un caso específico de la norma ISO / IEC 9797 cuando block size 64, MAC length 32, con MAC algorithm 1 o MAC algorithm 3 (ambos con padding method 1) [wikipedia.org], y el cifrado de bloque es DEA ("Data Encryption Algorithm"), como en ocasiones se denomina también a DES. [incits.org]

Cuando hablamos del cifrado de bloques DES, estamos hablando de un método de cifrado ya demasiado vulnerable hoy en día, debido al tamaño de clave de sólo 56 bits; además se sospecha de la existencia de alguna puerta trasera para la NSA ("National Security Agency"). Pero el caso del MAC en cuestión aplica un triple DES, también llamado 3DES ó DESede, lo que aumenta el espacio de claves dificultando su ataque. En concreto 3DES consiste en encriptar usando DES tres veces consecutivas: primero se cifra con una clave, después se descifra con otra y finalmente se vuelve a cifrar con una tercera. Este algoritmo se implementa en ANSI X9.19 de una forma un poco más particular, sólo se utilizan 2 claves y el triple DES sólo se aplica al último bloque:

  1. Rellenar el mensaje a cifrar con el menor número de '00' bytes a la derecha, de tal manera que la longitud del mensaje resultante sea múltiplo de 8 bytes.
  2. Cifrar el mensaje resultante del paso anterior con DES, la clave K1 y modo CBC.
  3. Descifrar el último bloque cifrado en el paso anterior con DES, la clave K2 y modo ECB.
  4. Cifrar el resultado del paso anterior con DES, la clave K1 y modo ECB.
  5. El MAC es el resultado del paso anterior. Para proteger la seguridad de la clave, el MAC puede restringirse, como era el caso que me ocupaba, a los 4 bytes más significativos del resultado obtenido.

Diagrama del cálculo del MAC

JAVA

En Java, el paquete javax.crypto incluye métodos de cifrado, pero no este DES-MAC directamente, así que o incorporamos a nuestro proyecto una librería externa como la de las Bouncy Castle Crypto APIs [bouncycastle.org], o seguimos los pasos descritos más arriba y lo implementamos nosotros a partir la arquitectura criptográfica proporcionada por Java.

Con Bouncy Castle:

Tendremos que incorporar a nuestro proyecto un jar de Providers (bcprov-jdk...jar) de the Legion of the Bouncy Castle Java cryptography APIs [https://www.bouncycastle.org/latest_releases.html], y por lo demás es bastante sencillo:
import org.bouncycastle.crypto.engines.DESEngine;
import org.bouncycastle.crypto.macs.ISO9797Alg3Mac;
import org.bouncycastle.crypto.params.DESedeParameters;
import org.bouncycastle.util.encoders.Hex;

import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;

public class CifradoBouncyCastle {

    public static void main(String[] args) throws UnsupportedEncodingException, NoSuchAlgorithmException {
        byte[] KEY_DATA = hexStringToByteArray("ABCDEF0101010101ABCDEF0202020202");
        DESEngine des = new DESEngine();
        ISO9797Alg3Mac mac = new ISO9797Alg3Mac(des, 32);
        mac.init(new DESedeParameters(KEY_DATA));

        byte[] data = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur cursus vulputate dapibus. Cras sollicitudin aliquet dui, ac pharetra arcu vehicula vel. Cras mattis ultrices ex.".getBytes("utf-8");
        byte[] result = new byte[mac.getMacSize()];

        mac.update(data, 0, data.length);
        mac.doFinal(result, 0);

        System.out.println("firma=" + new String(Hex.encode(result)));
    }

    private static byte[] hexStringToByteArray(String hex) {
        int len = hex.length();
        byte[] data = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4) + Character
                .digit(hex.charAt(i + 1), 16));
        }
        return data;
    }

}

Sin librerías externas:

No es difícil pero, después de buscar durante bastantes horas en Internet, no encontré ningún sitio que aportara una implementación en Java. Lo más parecido a ello es el "Retail MAC Calculation in Java" de Bharathi Subramanian, que tengo que reconocer, que aunque su código no era del todo correcto, me aportó bastante luz.

Ésta es por tanto mi verdadera aportación a todo esto. El código Java para calcular el ANSI X9.19 MAC de 4 bytes. Espero que os sirva de ayuda.
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
import javax.crypto.spec.IvParameterSpec;
import java.util.Arrays;

public class Cifrado {

    public static void main(String[] args) {
        try {
            String claveFirmar = "ABCDEF0101010101ABCDEF0202020202";
            byte[] claveFirmarBytes = hexStringToByteArray(claveFirmar);

            String textoFirmar = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur cursus vulputate dapibus. Cras sollicitudin aliquet dui, ac pharetra arcu vehicula vel. Cras mattis ultrices ex.";
            byte[] textoFirmarBytes = textoFirmar.getBytes("utf-8");

            byte[] allBytes = retailMac(claveFirmarBytes, textoFirmarBytes);
            byte[] firmaBytes = Arrays.copyOf(allBytes, 4);
            String firma = toHex(firmaBytes);

            System.out.println("firma=" + firma);
        } catch (Exception e) {
            e.getStackTrace();
        }
    }

    private static byte[] hexStringToByteArray(String hex) {
        int len = hex.length();
        byte[] data = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4) + Character
                .digit(hex.charAt(i + 1), 16));
        }
        return data;
    }

    final private static String toHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder(bytes.length * 2);
        for (byte b : bytes) {
            sb.append(String.format("%02x", b & 0xff));
        }
        return sb.toString();
    }

    private static byte[] retailMac(byte[] key, byte[] data) {
        try {
            // Create Keys
            byte[] key1 = Arrays.copyOf(key, 8);
            byte[] key2 = Arrays.copyOfRange(key, 8, 16);

            // ISO/IEC 9797-1 or ISO 7816d4 Padding for data (adding 80 00 ..)
            byte[] pdata = addPadding(data);

            SecretKeyFactory mySecretKeyFactory = SecretKeyFactory.getInstance("DES");

            DESKeySpec myKeySpec1 = new DESKeySpec(key1);
            SecretKey myKey1 = mySecretKeyFactory.generateSecret(myKeySpec1);
            Cipher cipher1 = Cipher.getInstance("DES/CBC/NoPadding");
            cipher1.init(Cipher.ENCRYPT_MODE, myKey1, new IvParameterSpec(
                    new byte[8]));

            DESKeySpec myKeySpec2 = new DESKeySpec(key2);
            SecretKey myKey2 = mySecretKeyFactory.generateSecret(myKeySpec2);
            Cipher cipher2 = Cipher.getInstance("DES/CBC/NoPadding");
            cipher2.init(Cipher.DECRYPT_MODE, myKey2, new IvParameterSpec(
                    new byte[8]));

            byte[] result = cipher1.doFinal(pdata);

            byte[] block = Arrays.copyOfRange(result, result.length - 8, result.length);

            // Decrypt the resulting block with Key-2
            block = cipher2.doFinal(block);

            // Encrypt the resulting block with Key-1
            block = cipher1.doFinal(block);

            return block;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    private static byte[] addPadding(byte[] in) {
        int extra = 8 - (in.length % 8);
        int newLength = in.length + extra;
        byte[] out = Arrays.copyOf(in, newLength);
        int offset = in.length;
//        out[offset] = (byte) 0x80;
//        offset++;
        while (offset < newLength) {
            out[offset] = (byte) 0;
            offset++;
        }
        return out;
    }

}

UPDATE:

Ver http://raulcad.blogspot.co.uk/2014/08/convertir-array-de-bytes-en-string-de.html para ver un método más óptimo que el "toHex()" al convertir un array de bytes en un String hexadecimal.

2013-04-23

Gestión de datos en PHP

Os voy a exponer como hago yo la gestión de datos en PHP, apoyándome en el modelado con clases y el patrón DAO (Data Access Object).

Los Objetos de Acceso a Datos son un patrón de diseño J2EE considerados una buena práctica. Un DAO permite abstraer y encapsular todos los accesos a la fuente de datos (normalmente una base de datos, como MySQL). EL DAO gestiona la conexión con la fuente de datos para obtener y almacenar datos, implementando los mecanismos de acceso requeridos.  El DAO oculta los detalles de implementación del origen de datos a sus clientes. Debido a que la interfaz expuesta por el DAO a los clientes no cambia cuando cambia la implementación del origen de datos suybacente, este modelo permite al DAO adaptarse a diferentes esquemas de almacenamiento sin afectar a sus clientes o componentes de negocio.

El siguiente diagrama de clases representa las relaciones del patrón DAO:
Y en el siguiente diagrama de secuencia se representa la interacción entre los distintos participantes del patrón:
El BusinessObject es el cliente que requiere el acceso a la fuente de datos para obtener y almacenar datos. El DataAccessObject es en quien delega el BusinessObject para hacer estas operaciones. El DataSource representa una implementación de una fuente de datos (como un RDBMS). El TransferObject representa un soporte temporal de datos. El DataAccessObject puede utilizar un TransferObject para devolver datos al cliente o recibir los datos del cliente en un TransferObject para actualizar los datos del DataSource.


Esta es la manera en que yo aplico el patrón DAO en PHP para gestionar los datos en las aplicaciones de forma uniforme, encapsulada y sencilla:

Voy a utilizar como ejemplo la siguiente tabla FABRICANTES:
(Ver: Modelo de datos)
CREATE TABLE fabricantes (
  id smallint(5) NOT NULL AUTO_INCREMENT,
  codigo varchar(10) NOT NULL,
  denominacion_social varchar(100) NOT NULL,
  activo tinyint(1) NOT NULL DEFAULT '1',
  PRIMARY KEY (id)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;
  1. Por cada tabla creamos dos clases PHP:
    (Para no tener que hacer includes de las clasese en los scripts, ver: )
    • class.fabricante.php, que será el TransferObject, con variables para cada campo de la tabla:
    <?php
    class Fabricante extends Desactivable {
     
    public $id;
    public $codigo;
    public $denominacion_social;
    
    }
    ?>
    
    • class.fabricantes.php, que será el DataAccessObject
    <?php
    define('FABRICANTES_FIELDS_SIN_ID', 'codigo, denominacion_social');
    define('FABRICANTES_FIELDS', 'id, ' . FABRICANTES_FIELDS_SIN_ID);
    define('FABRICANTES_SELECT', 'SELECT ' . FABRICANTES_FIELDS . ' FROM fabricantes WHERE 1 = 1');
    define('FABRICANTES_ORDER', ' ORDER BY denominacion_social');
    define('FABRICANTES_INSERT', 'INSERT INTO fabricantes (' . FABRICANTES_FIELDS_SIN_ID . ') VALUES (%s, %s)');
    define('FABRICANTES_UPDATE', 'UPDATE fabricantes SET codigo = %s, denominacion_social = %s WHERE id = %s');
    define('FABRICANTES_DELETE', 'UPDATE fabricantes SET activo = 0 WHERE activo = 1');
    
    class Fabricantes {
    
     function getFabricante($id, $activo = true) {
      $sql = sprintf(FABRICANTES_SELECT . ($activo? ' AND activo = 1' : '') . ' AND id = %s',
        param($id));
      $result = mysql_query($sql) or trigger_error(mysql_error());
      if ($obj = mysql_fetch_object($result, 'Fabricante')) {
       return $obj;
      }
    
      return false;
     }
    
     function getFabricantes($fab = null, $orden = null, $ordenado = null) {
      $sql = FABRICANTES_SELECT . ' AND activo = 1';
      if (isset($fab)) {
       if ($fab->codigo != '') {
        $sql .= sprintf(' AND codigo = %s',
          param($fab->codigo));
       }
      }
      if (isset($orden)) {
       $sql .= ' ORDER BY ' . $orden . '';
       if (isset($ordenado)) {
        $sql .= ' ' . $ordenado;
       }
      } else {
       $sql .= FABRICANTES_ORDER;
      }
      $result = mysql_query($sql) or trigger_error(mysql_error());
      while ($obj = mysql_fetch_object($result, 'Fabricante')) {
       $objs[] = $obj;
      }
     
      if (isset($objs)) {
       return $objs;
      }
      return false;
     } 
    
     function setFabricante($fab) {
      if ($fab->id == null) {
       $sql = sprintf(FABRICANTES_INSERT,
         param($fab->codigo), param($fab->denominacion_social));
      } else {
       $sql = sprintf(FABRICANTES_UPDATE,
         param($fab->codigo), param($fab->denominacion_social), $fab->id);
      }
      mysql_query($sql) or trigger_error(mysql_error());
      if (mysql_affected_rows() === 1) {
       if ($fab->id == null) {
        $fab->id = mysql_insert_id();
       }
       return true;
      }
     
      return false;
     } 
    
     function remFabricantes($ids) {
      $sql = sprintf(FABRICANTES_DELETE . ' AND id IN (%s)', $ids);
      mysql_query($sql) or trigger_error(mysql_error());
      return mysql_affected_rows();
     } 
     
    }
    ?>
    
  2. Uso desde un cliente o BusinessObject:
    (Ver Autocarga de clases en PHP)
    <?php
    $fabMgr = new Fabricantes();
    if ($_POST['id'] != '') {
     $fab = $fabMgr->getFabricante($_POST['id']);
    } else {
     $fab = new Fabricante();
    }
    
    $fab->codigo = $_POST['codigo'];
    $fab->denominacion_social = $_POST['denominacion_social'];
     
    if ($fabMgr->setFabricante($fab)) { 
     $cambios = $fab->id;
    } else {
     $errores = 'Fabricante no guardado';
    }
    ?>
    

2013-03-03

Autocarga de clases en PHP

Cuando creamos un fichero fuente PHP para cada definición de clase y queremos evitar tener que incluir al comienzo de cada script todos los includes necesarios de las clases que usan, se puede definir una función __autoload(), que es automáticamente invocada en caso de que se esté intentando utilizar una clase que no ha sido definida. Esta función podríamos meterla en un fichero que se incluya siempre al principio de cada script y tendría, por ejemplo, esta definición:
<?php
function __autoload($nombre_clase) {
 include 'dao/class.' . $nombre_clase . '.php';
}
?>
Entonces, en lugar de tener un script así:
<?php
require('dao/class.cliente.php');
require('dao/class.clientes.php');
require('dao/class.pedido.php');
require('dao/class.pedidos.php');
require('dao/class.producto.php');
require('dao/class.productos.php');
require('dao/class.almacen.php');
require('dao/class.almacenes.php');

$cliente = new Cliente();

$pedido = new Pedido($cliente);

...
?>
Podemos tener:
<?php
require('base/start.php');

$cliente = new Cliente();

$pedido = new Pedido($cliente);

...
?>

NOTA: A partir de PHP 5.1.2 se recomienda el uso de spl_autoload_register() como alternativa a __autoload() [php.net]

2013-02-01

Cómo configurar un directorio virtual en Apache

Mediante directorios virtuales podemos acceder a los recursos que están fuera del directorio raíz del servidor (DocumentRoot), por ejemplo en C:\ruta\a\directorio mediante una url como http://localhost/mialias

Yo utilizo esto, por ejemplo, para tener la versión en desarrollo de los proyectos en una ruta distinta a la de la instalación del servidor pero poder probarla directamente en el servidor.

Para configurar un directorio virtual en Apache tenemos que:
  1. Abrir el fichero httpd.conf
  2. Buscar el bloque
    <IfModule alias_module>
    
  3. Y después de
    ScriptAlias /cgi-bin/ "C:/xampp/cgi-bin/"
    
  4. Añadir la siguiente línea:
    Alias "/mialias" "C:\ruta\a\directorio"
    
  5. Por último, después de
    <Directory "C:/xampp/cgi-bin">
     AllowOverride None
     Options None
     Require all granted
    </Directory>
    
  6. Añadir el siguiente bloque:
    <Directory "C:\ruta\a\directorio">
     Options Indexes FollowSymLinks Includes ExecCGI
     AllowOverride All
     Order allow,deny
     Allow from all
     Require all granted
    </Directory>
    
  7. Reiniciando Apache ya podremos navegar a http://localhost/mialias

2013-01-24

Clases para un modelo de datos en PHP

Para los modelos de datos que presenté (Modelo de datos) presento el conjunto de clases que lo soportan haciendo uso de la herencia.

Primero un diagrama:

// TODO

Y ahora el código de las clases, junto con las 2 clases de ejemplo, MiClaseDesactivable y MiClaseHistoriable:

<?php
abstract class Desactivable {
 
 public $activo;
 public $alta;
 public $baja;
 
}
?>
<?php
abstract class Historiable extends Desactivable {
 
 public $inicio;
 public $fin;
 
}
?>
Como me gusta hacer borrados lógicos en vez de físicos en las tablas de datos, todas las clases de mis proyectos heredarán de una de éstas 2 clases abstractas, directamente de Desactivable, para los casos generales, o de Historiable, para tablas versionadas o históricas (Slowly Changing Dimension de tipo 2). Ver http://raulcad.blogspot.com.es/2013/01/modelo-de-datos.html
<?php
class MiClaseDesactivable extends Desactivable {
 
 // TODO añadir los campos propios
 
}
?>
<?php
class MiClaseHistoriable extends Historiable {
 
 // TODO añadir los campos propios
 
}
?>