Leer los datos públicos del DNIe

Me acabo de renovar el DNI (Documento Nacional de Identidad) y me lo dieron con su "chip" y su número PIN, así que aprovechando que tenia un lector de tarjetas guardado en un cajón desde hace mas de un año me propuse darle uso. Primero instale todo el software necesario para usarlo con el navegador, firmar documentos, etc ... pero pronto me aburrí, así que intente buscar la forma de acceder a los datos del DNIe desde Delphi (mi lenguaje de programación preferido). Pero no quería jugar con los "datos privados" contenidos en la tarjeta ya que para eso hace falta introducir el PIN y corría el riesgo, si me equivocaba al programar, de bloquear el DNIe y tener que ir a desbloquearlo a una comisaria de policía. Así que me limite a su "zona publica" donde, entre otras cosas, podemos encontrar el numero del documento, nuestro nombre y apellidos, la fecha de expedición, etc ...

El programa es sencillo, utiliza las funciones de la API de windows "Winscard" para acceder al lector y a la tarjeta, y así leer el contenido de los ficheros públicos.

Lo primero es encontrar el lector y comprobar que tiene un DNIe dentro:

var
  hContext: SCARDCONTEXT;
  pmszReaders: PAnsiChar;
  pReader: PAnsiChar;
  cchReaders: DWORD;
  lReturn: Longint;
  Card: SCARDHANDLE;
  ActiveProtocol: DWORD;
  Stream: TMemoryStream;
begin
  lReturn:= SCardEstablishContext(SCARD_SCOPE_SYSTEM,nil,nil,@hContext);
  if lReturn = SCARD_S_SUCCESS then
  try
    cchReaders:= SCARD_AUTOALLOCATE;
    // Obtenemos una lista con todos los lectores
    lReturn:= SCardListReaders(hContext,nil,@pmszReaders,@cchReaders);
    if lReturn = SCARD_S_SUCCESS then
    begin
      pReader:= pmszReaders;
      // Intentamos conectarnos a cada uno de los lectores de la lista
      while (pReader^ <> #0) do
      begin
        lReturn:= SCardConnect(hContext, pReader, SCARD_SHARE_SHARED,
          SCARD_PROTOCOL_T0, @Card, @ActiveProtocol);
        if lReturn = SCARD_S_SUCCESS then
        begin
          // Comprobamos si la tarjeta insertada es un DNIe
          if isDNIe(Card) then
          begin
            // Si llegamos hasta aquí es que hemos encontrado el DNIe
            Writeln('DNIe Encontrado');
            Writeln;            
            SCardDisconnect(Card,SCARD_RESET_CARD);
            break;
          end;
          SCardDisconnect(Card,SCARD_RESET_CARD);
        end;
        pReader:= pReader + StrLen(pReader) + 1;
      end;
      SCardFreeMemory(hContext,pmszReaders);
    end;
  finally
    SCardReleaseContext(hContext);
  end;
  Writeln('Adios!');
end.

La función que utilizamos para comprobar si es un DNIe es esta:

function isDNIe(hCard: SCARDHANDLE): Boolean;
var
  bAtr: PByteArray;
  cbAtrLen: DWORD;
  lReturn: Longint;
begin
  bAtr:= nil;
  cbAtrLen:= 0;
  lReturn:= SCardStatus(hCard, nil,nil,nil,nil,LPBYTE(bAtr),@cbAtrLen);
  if (lReturn <> SCARD_S_SUCCESS) or (cbAtrLen<>20) then
  begin
    Result:= FALSE;
    Exit;
  end;
  GetMem(bAtr,cbAtrLen);
  try
    lReturn:= SCardStatus(hCard, nil,nil,nil,nil,LPBYTE(bAtr),@cbAtrLen);
    if lReturn <> SCARD_S_SUCCESS then
    begin
      Result:= FALSE;
      Exit;
    end;
    Result:=
      (bAtr[00]=$3B) and (bAtr[01]=$7F) and (bAtr[03]=$00) and (bAtr[04]=$00) and
      (bAtr[05]=$00) and (bAtr[06]=$6A) and (bAtr[07]=$44) and (bAtr[08]=$4E) and
      (bAtr[09]=$49) and (bAtr[10]=$65) and (bAtr[18]=$90) and (bAtr[19]=$00);
  finally
    FreeMem(bAtr);
  end;
end;

Comprueba la cadena ATR de la tarjeta con la que debe tener un DNIe: http://www.dnielectronico.es/PDFs/ATR1.pdf

Una vez que estamos conectados a la tarjeta ya podemos leer sus ficheros, para eso utilizo estas dos funciones:

// Envia un comando a la tarjeta (iso_7816-4v2_0.pdf)
function SendCmd(Card: SCARDHANDLE; Cmd: AnsiString; Buffer: PByte; var l: DWORD): Boolean;
begin
  Result:= FALSE;
  if SCardTransmit(Card, @SCARD_PCI_T0,PAnsiChar(Cmd),Length(Cmd), nil,
    LPBYTE(Buffer), @l) = SCARD_S_SUCCESS then
    if l >= 2 then
      Result:= (PByteArray(Buffer)[l-2] = $61) or
        ((PByteArray(Buffer)[l-2] = $90) and (PByteArray(Buffer)[l-1] = $00));
end;
 
// obtiene_CDF.pdf
function SCardReadFile(Card: SCARDHANDLE; Path: AnsiString; Stream: TStream): Boolean;
var
  Buffer: PByte;
  l: DWORD;
  Offset: Word;
  Size: Word;
begin
  Result:= FALSE;
  if Odd(Length(Path)) then
    Exit;
  GetMem(Buffer,258);
  try
    l:= 258;
    // SELECT directorio raiz
    if not SendCmd(Card,
      #$00#$a4#$04#$00#$0b#$4D#$61#$73#$74#$65#$72#$2E#$46#$69#$6C#$65,
      Buffer, l) then Exit;
    // Seguimos la ruta del fichero
    while Length(Path) > 0 do
    begin
      l:= 258;
      // SELECT
      if not SendCmd(Card,#$00#$A4#$00#$00#$02 + Copy(Path,1,2),
        Buffer, l) then Exit;
      delete(Path,1,2);
    end;
    l:= 258;
    // GET RESPONSE
    if not SendCmd(Card,#$00#$C0#$00#$00#$0E,Buffer, l) then
      Exit;
    if (l <> $10) or (PByteArray(buffer)[0] <> $6F) then
      Exit;
    // Obtenemos el tamaño
    Size:= (PByteArray(buffer)[7] * $100) + PByteArray(buffer)[8];
    Offset:= 0;
    while Offset < Size do
    begin
      l:= 258;
      // Leemos el fichero en bloques de hasta 255 bytes
      if not SendCmd(Card,#$00#$B0 + AnsiChar(Hi(Offset)) + AnsiChar(Lo(Offset))
        + Char(Min(Size-Offset,$FF)), Buffer, l) then Exit;
      if l > 2 then
        Stream.Write(Buffer^,l-2);
      inc(Offset,$FF);
    end;
    Result:= TRUE;
  finally
    FreeMem(Buffer);
  end;
end;

El parámetro "Path" es la ruta del fichero en hexadecimal a partir del directorio raíz. Por ejemplo la ruta del IDESP es "0006". Puedes encontrar mas rutas aquí:
http://opendnie.morfeo-project.org/wiki/index.php/Documentacion_DNIe_Est...

Algunas interesantes son:

0006         IDESP
50156004  PKCS15-CDF

El código fuente completo es este (es una aplicación de consola):

program DNIeReader;
 
{$APPTYPE CONSOLE}
 
uses
  Windows,
  SysUtils,
  Classes,
  WinSCard in 'WinSCard.pas';
 
 
function Min(i,j: Integer): Integer;
begin
  if i < j then
    Result:= i
  else
    Result:= j;
end;
 
// Muestra el contenido del buffer en hexadecimal y como texto
procedure WriteHex(Buffer: PAnsiChar;  Count: Integer);
var
  i,j: Integer;
begin
  j:= 0;
  while Count > 0 do
  begin
    Write(IntToHex(j,8) + ':' + #32#32);
    for i:= 0 to Min(Count,8) - 1 do
      Write(IntToHex(Byte(Buffer[i]),2) + #32);
    Write(#32);
    for i:= 8 to Min(Count,16) - 1 do
      Write(IntToHex(Byte(Buffer[i]),2) + #32);
    for i:= Min(Count,16) to 15 do
      Write(#32#32#32);
    Write(#32 + '|');
    for i:= 0 to Min(Count,16) - 1 do
    if Char(Buffer[i]) in ['A'..'Z','a'..'z','0'..'9'] then
      Write(Buffer[i])
    else
      Write('.');
    Writeln('|');
    Dec(Count,16);
    inc(Buffer,16);
    inc(j,16);
  end;
end;
 
function isDNIe(hCard: SCARDHANDLE): Boolean;
var
  bAtr: PByteArray;
  cbAtrLen: DWORD;
  lReturn: Longint;
begin
  bAtr:= nil;
  cbAtrLen:= 0;
  lReturn:= SCardStatus(hCard, nil,nil,nil,nil,LPBYTE(bAtr),@cbAtrLen);
  // ATR1.pdf
  if (lReturn <> SCARD_S_SUCCESS) or (cbAtrLen<>20) then
  begin
    Result:= FALSE;
    Exit;
  end;
  GetMem(bAtr,cbAtrLen);
  try
    lReturn:= SCardStatus(hCard, nil,nil,nil,nil,LPBYTE(bAtr),@cbAtrLen);
    if lReturn <> SCARD_S_SUCCESS then
    begin
      Result:= FALSE;
      Exit;
    end;
    // ATR1.pdf
    Result:=
      (bAtr[00]=$3B) and (bAtr[01]=$7F) and (bAtr[03]=$00) and (bAtr[04]=$00) and
      (bAtr[05]=$00) and (bAtr[06]=$6A) and (bAtr[07]=$44) and (bAtr[08]=$4E) and
      (bAtr[09]=$49) and (bAtr[10]=$65) and (bAtr[18]=$90) and (bAtr[19]=$00);
  finally
    FreeMem(bAtr);
  end;
end;
 
// Envia un comando a la tarjeta (iso_7816-4v2_0.pdf)
function SendCmd(Card: SCARDHANDLE; Cmd: AnsiString; Buffer: PByte; var l: DWORD): Boolean;
begin
  Result:= FALSE;
  if SCardTransmit(Card, @SCARD_PCI_T0,PAnsiChar(Cmd),Length(Cmd), nil,
    LPBYTE(Buffer), @l) = SCARD_S_SUCCESS then
    if l >= 2 then
      Result:= (PByteArray(Buffer)[l-2] = $61) or
        ((PByteArray(Buffer)[l-2] = $90) and (PByteArray(Buffer)[l-1] = $00));
end;
 
// obtiene_CDF.pdf
function SCardReadFile(Card: SCARDHANDLE; Path: AnsiString; Stream: TStream): Boolean;
var
  Buffer: PByte;
  l: DWORD;
  Offset: Word;
  Size: Word;
begin
  Result:= FALSE;
  if Odd(Length(Path)) then
    Exit;
  GetMem(Buffer,258);
  try
    l:= 258;
    // SELECT directorio raiz
    if not SendCmd(Card,
      #$00#$a4#$04#$00#$0b#$4D#$61#$73#$74#$65#$72#$2E#$46#$69#$6C#$65,
      Buffer, l) then Exit;
    // Seguimos la ruta del fichero
    while Length(Path) > 0 do
    begin
      l:= 258;
      // SELECT
      if not SendCmd(Card,#$00#$A4#$00#$00#$02 + Copy(Path,1,2),
        Buffer, l) then Exit;
      delete(Path,1,2);
    end;
    l:= 258;
    // GET RESPONSE
    if not SendCmd(Card,#$00#$C0#$00#$00#$0E,Buffer, l) then
      Exit;
    if (l <> $10) or (PByteArray(buffer)[0] <> $6F) then
      Exit;
    // Obtenemos el tamaño
    Size:= (PByteArray(buffer)[7] * $100) + PByteArray(buffer)[8];
    Offset:= 0;
    while Offset < Size do
    begin
      l:= 258;
      // Leemos el fichero en bloques de hasta 255 bytes
      if not SendCmd(Card,#$00#$B0 + AnsiChar(Hi(Offset)) + AnsiChar(Lo(Offset))
        + Char(Min(Size-Offset,$FF)), Buffer, l) then Exit;
      if l > 2 then
        Stream.Write(Buffer^,l-2);
      inc(Offset,$FF);
    end;
    Result:= TRUE;
  finally
    FreeMem(Buffer);
  end;
end;
 
var
  hContext: SCARDCONTEXT;
  pmszReaders: PAnsiChar;
  pReader: PAnsiChar;
  cchReaders: DWORD;
  lReturn: Longint;
  Card: SCARDHANDLE;
  ActiveProtocol: DWORD;
  Stream: TMemoryStream;
begin
  lReturn:= SCardEstablishContext(SCARD_SCOPE_SYSTEM,nil,nil,@hContext);
  if lReturn = SCARD_S_SUCCESS then
  try
    cchReaders:= SCARD_AUTOALLOCATE;
    // Obtenemos una lista con todos los lectores
    lReturn:= SCardListReaders(hContext,nil,@pmszReaders,@cchReaders);
    if lReturn = SCARD_S_SUCCESS then
    begin
      pReader:= pmszReaders;
      // Intentamos conectarnos a cada uno de los lectores de la lista
      while (pReader^ <> #0) do
      begin
        lReturn:= SCardConnect(hContext, pReader, SCARD_SHARE_SHARED,
          SCARD_PROTOCOL_T0, @Card, @ActiveProtocol);
        if lReturn = SCARD_S_SUCCESS then
        begin
          // Comprobamos si la tarjeta insertada es un DNIe
          if isDNIe(Card) then
          begin
            Writeln('DNIe Encontrado');
            Writeln;
            Stream:= TMemoryStream.Create;
            try
              // Leemos el IDESP con la ruta 0006
              if SCardReadFile(Card,#$00#$06,Stream) then
              begin
                Writeln('IDESP (0006):');
                WriteHex(Stream.Memory,Stream.Size);
                Writeln;
              end;
              Stream.Clear;
              // Leemos la version con la ruta 2F03
              if SCardReadFile(Card,#$2F#$03,Stream) then
              begin
                Writeln('Version (2F03):');
                WriteHex(Stream.Memory,Stream.Size);
                Writeln;
              end;
              Stream.Clear;
              // Leemos el CDF con la ruta 50156004
              if SCardReadFile(Card,#$50#$15#$60#$04,Stream) then
              begin
                Writeln('CDF (50156004):');
                WriteHex(Stream.Memory,Stream.Size);
                Writeln;
              end;
            finally
              Stream.Free;
            end;
            SCardDisconnect(Card,SCARD_RESET_CARD);
            break;
          end;
          SCardDisconnect(Card,SCARD_RESET_CARD);
        end;
        pReader:= pReader + StrLen(pReader) + 1;
      end;
      SCardFreeMemory(hContext,pmszReaders);
    end;
  finally
    SCardReleaseContext(hContext);
  end;
  Writeln('Adios!');
end.

La unit WinSCard.pas la cree para importar algunas funciones y constantes de la API

unit WinSCard;
 
interface
 
uses Windows, Sysutils;
 
type
  SCARDHANDLE   = ULONG;
  PSCARDHANDLE  = ^SCARDHANDLE;
  LPSCARDHANDLE = PSCARDHANDLE;
 
  SCARDCONTEXT   = ULONG;
  PSCARDCONTEXT  = ^SCARDCONTEXT;
  LPSCARDCONTEXT = PSCARDCONTEXT;
 
  LPBYTE = ^BYTE;
 
  SCARD_IO_REQUEST = record
    dwProtocol: ULONG;
    cbPciLength: ULONG;
  end;
  PSCARD_IO_REQUEST = ^SCARD_IO_REQUEST;
  LPSCARD_IO_REQUEST = PSCARD_IO_REQUEST;
  LPCSCARD_IO_REQUEST = PSCARD_IO_REQUEST;
 
const
  WinSCard_dll = 'Winscard.dll';
 
  MAX_ATR_SIZE = 3;
 
  SCARD_PROTOCOL_T0	= $0001;
  SCARD_PROTOCOL_T1 = $0002;
  SCARD_PROTOCOL_RAW = $0004;
 
  SCARD_STATE_UNAWARE = $0000;
  SCARD_STATE_IGNORE = $0001;
  SCARD_STATE_CHANGED = $0002;
  SCARD_STATE_UNKNOWN = $0004;
  SCARD_STATE_UNAVAILABLE = $0008;
  SCARD_STATE_EMPTY = $0010;
  SCARD_STATE_PRESENT = $0020;
  SCARD_STATE_EXCLUSIVE = $0080;
  SCARD_STATE_INUSE = $0100;
  SCARD_STATE_MUTE = $0200;
  SCARD_STATE_UNPOWERED = $0400;
 
 
  SCARD_SHARE_EXCLUSIVE = $0001;
  SCARD_SHARE_SHARED = $0002;
  SCARD_SHARE_DIRECT = $0003;
 
  SCARD_LEAVE_CARD = $0000;
  SCARD_RESET_CARD = $0001;
  SCARD_UNPOWER_CARD = $0002;
 
  SCARD_AUTOALLOCATE = DWORD(-1);
 
  SCARD_SCOPE_USER = 0;
  SCARD_SCOPE_TERMINAL = 1;
  SCARD_SCOPE_SYSTEM = 2;
 
  SCARD_PCI_T0: SCARD_IO_REQUEST =
    (dwProtocol: SCARD_PROTOCOL_T0; cbPciLength: SizeOf(SCARD_IO_REQUEST));
  SCARD_PCI_T1: SCARD_IO_REQUEST =
    (dwProtocol: SCARD_PROTOCOL_T1; cbPciLength: SizeOf(SCARD_IO_REQUEST));
  SCARD_PCI_RAW: SCARD_IO_REQUEST =
    (dwProtocol: SCARD_PROTOCOL_RAW; cbPciLength: SizeOf(SCARD_IO_REQUEST));
 
  SCARD_S_SUCCESS	 = $00000000;
  SCARD_E_CANCELLED = $80100002;
  SCARD_E_INVALID_HANDLE = $80100003;
  SCARD_E_INVALID_PARAMETER = $80100004;
  SCARD_E_TIMEOUT	 = $8010000A;
  SCARD_E_SHARING_VIOLATION	= $8010000B;
  SCARD_E_NO_SMARTCARD = $8010000C;
  SCARD_E_PROTO_MISMATCH = $8010000F;
  SCARD_E_NOT_TRANSACTED = $80100016;
  SCARD_E_READER_UNAVAILABLE = $8010001;
  SCARD_E_NO_SERVICE = $8010001D;
  SCARD_E_NO_READERS_AVAILABLE = $8010002E;
  SCARD_W_UNRESPONSIVE_CARD = $80100066;
  SCARD_W_UNPOWERED_CARD = $80100067;
  SCARD_W_RESET_CARD = $80100068;
  SCARD_W_REMOVED_CARD = $80100069;
 
 
function SCardEstablishContext(dwScope: DWORD; pvReserved1: Pointer;
  pvReserved2: Pointer; phContext: LPSCARDCONTEXT): Longint; stdcall;
  external WinSCard_dll;
 
function SCardReleaseContext(hContext: SCARDCONTEXT): Longint; stdcall;
  external WinSCard_dll;
 
function SCardListReaders(hContext: SCARDCONTEXT; mszGroups: LPCTSTR;
  mszReaders: Pointer; pcchReaders: LPDWORD): Longint; stdcall;
  external WinSCard_dll name 'SCardListReadersA';  // (ANSI)
 
function SCardFreeMemory(hContext: SCARDCONTEXT; pvMem: Pointer): Longint;
  stdcall; external WinSCard_dll;
 
function SCardConnect(hContext: SCARDCONTEXT; szReader: LPCTSTR;
  dwShareMode: DWORD; dwPreferredProtocols: DWORD; phCard: LPSCARDHANDLE;
  pdwActiveProtocol: LPDWORD): Longint; stdcall;
  external WinSCard_dll name 'SCardConnectA';  // (ANSI)
 
function SCardDisconnect(hCard: SCARDHANDLE; dwDisposition: DWORD): Longint;
  stdcall; external WinSCard_dll;
 
function SCardStatus(hCard: SCARDHANDLE; szReaderName: LPTSTR;
  pcchReaderLen: LPDWORD; pdwState: LPDWORD; pdwProtocol: LPDWORD;
  pbAtr: LPBYTE; pcbAtrLen: LPDWORD): Longint; stdcall;
  external WinSCard_dll name 'SCardStatusA';  // (ANSI)
 
function SCardTransmit(hCard: SCARDHANDLE; pioSendPci: LPCSCARD_IO_REQUEST;
  pbSendBuffer: Pointer; cbSendLength: DWORD; pioRecvPci: LPSCARD_IO_REQUEST;
  pbRecvBuffer: LPBYTE; pcbRecvLength: LPDWORD): Longint;
  stdcall; external WinSCard_dll;
 
implementation
 
end.

El código fuente completo, la unit WinSCard y el programa ya compilado lo puedes bajar de aquí

Para saber más:
http://www.dnielectronico.es/PDFs/ATR1.pdf
http://www.dnie.es/PDFs/obtiene_CDF.pdf
https://zonatic.usatudni.es/es/aprendizaje/aprende-sobre-el-dnie/58-desa...
http://blog.48bits.com/2010/03/16/analisis-de-la-estructura-interna-del-...
http://opendnie.morfeo-project.org/wiki/index.php/Documentacion_DNIe_Est...
http://opendnie.cenatic.es/wiki/index.php/Documentacion_DNIe_Comandos

Comentarios

Hola, estoy intentando ejecutar tu programa en Delphi Xe2 pero no me va, en primer lugar me salen dos errores de compilacion en pmszReaders: PAnsiChar y pReader: PAnsiChar me dice E2010 Incompatible types - PwideChar and Pansichar, así que lo que he hecho es cambiar la deficnión de PAnsiChar por PWideChar y ya me ha compilado, no se si es una barbaridad....

y ahora al probar el programa, el lector me lo detecta, pero en la función SCardConnect me devuelve el valor 1117....

Alguna idea?

Gracias.

El error ¿en que linea exactamente te da?

Si es aqui:

pReader^ <> #0

prueba con esto:

pReader^ <> AnsiChar(#0)

o con esto:

Byte(pReader^) <> 0

Lo de cambiar los tipos a PWideChar no va a funcionar porque las funciones de la API que uso son ANSI, puede que usando las mismas fuciones pero en su version unicode funcionase bien, pero seguramente habría que cambiar algunas cosas mas en el codigo.

Prueba con los cambios que te dije, y si no te va, dime exactamente en que linea te salta el error.

Saludos

Se me olvidaba, cambia tambien en la unidad WinSCard.pas "LPCTSTR" y "LPTSTR" por "PAnsiChar"

Gracias por tus artículos Domingo: son todos excelentes, como no puede ser de otra forma viniendo de ti.

Gracias por el interesantísimo artículo.
Me gustaría informarme si existe alguna forma de acceder y descargar la fotografía del usuario, igualmente a como haces en este ejemplo, sin solicitar el pin al usuario.
Sería una forma genial de actualizar la ficha del cliente en la aplicación que estoy desarrollando.
¿Alguien ha conseguido o investigado esta posibilidad?
Gracias a todos.

Pues CREO que ese tipo de información no es accesible, ni incluso utilizando el PIN.

Tengo entendido que solo se puede acceder a estos datos desde los puestos que se encuentran en las comisarias de policía.