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
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
El error ¿en que linea exactamente te da?
Si es aqui:
prueba con esto:
o con esto:
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
Se me olvidaba, cambia tambien en la unidad WinSCard.pas "LPCTSTR" y "LPTSTR" por "PAnsiChar"
Gracias por tus artículos
Gracias por tus artículos Domingo: son todos excelentes, como no puede ser de otra forma viniendo de ti.
Gracias por el
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
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.