Análisis del contenido de los códigos QR en documentos del gobierno electrónico de la República de Kazajstán en la interfaz

Demostraré cómo usar JavaScript directamente en el navegador puede extraer y analizar datos de códigos QR contenidos en documentos generados por los portales electrónicos del gobierno de la República de Kazajstán (por ejemplo, https://egov.kz ).


En documentos electrónicos, la siguiente redacción está presente:


* el código de barras contiene datos recibidos del sistema de información del GDB PH y firmados con una firma digital electrónica de la Sucursal de la Corporación Estatal "Gobierno para Ciudadanos" NAO.

Hasta donde yo sé, no hay herramientas listas para extraer y analizar datos en códigos QR.


Mi objetivo final es extraer los datos firmados y la firma, verificar la integridad de los datos firmados. No se hablará de verificar una firma digital en esta nota, solo de verificar un hash. Los detalles de la verificación de firmas digitales pueden describirse en el futuro, en caso de que el público muestre interés en este tema.


Importante: esta nota no describe las técnicas de piratería y no ayuda a obtener acceso no autorizado a los datos; hablaremos sobre la conversión de datos de una vista a otra.


Mostraré cómo procesar los archivos PDF originales que forman y proporcionan portales de descarga del gobierno electrónico de la República de Kazajstán. Estos archivos PDF contienen códigos QR como imágenes incrustadas separadas.


.


:



0. PDF ArrayBuffer


PDF HTML <input type="file"> files.


ArrayBuffer :


const fileContents = await fileInput.files[0].arrayBuffer();

1. PDF


PDF.js , https://mozilla.imtqy.com/pdf.js/examples/index.html#interactive-examples


const pdfjsLib = window['pdfjs-dist/build/pdf'];
pdfjsLib.GlobalWorkerOptions.workerSrc = 'pdf.worker.js';

PDF.js . , :


const ops = [
  pdfjsLib.OPS.paintJpegXObject,
  pdfjsLib.OPS.paintImageXObject,
];

PDF :


const loadingTask = pdfjsLib.getDocument(fileContents);
const pdf = await loadingTask.promise;

const objIDs = [];
const images = [];

await (async function () {
  for (let pageIndex = 1; pageIndex <= pdf.numPages; pageIndex += 1) {
    const page = await pdf.getPage(pageIndex);

    //    ,   .
    const operators = await page.getOperatorList();
    for (let i = 0; i < operators.fnArray.length; i++) {
      const fn = operators.fnArray[i];

      if (ops.indexOf(fn) !== -1) {
        //       ,   -  .
        const objID = operators.argsArray[i][0];

        //          ,   .
        if (objIDs.indexOf(objID) === -1) {
          objIDs.push(objID);

          //       .
          try {
            const imageInfo = page.objs.get(objID);
            images.push(imageInfo);
          } catch (err) {
            console.log(err);
          }
        }
      }
    }
  }
})()

2. QR


jsQR RGBA PDF RGB, RGB RGBA:


function extractRGBAData(image) {
  if (image.kind === 3) { // ImageKind.RGBA_32BPP  https://github.com/mozilla/pdf.js/blob/master/src/shared/util.js
    return image.data;
  }

  if (image.kind !== 2) { // ImageKind.RGB_24BPP  https://github.com/mozilla/pdf.js/blob/master/src/shared/util.js
    throw new Error(`Image kind "${image.kind}" is not supported.`);
  }

  const data = new Uint8ClampedArray(image.width * image.height * 4);

  let destPosition = 0;
  for (let srcPosition = 0; srcPosition < image.data.length;) {
    data[destPosition++] = image.data[srcPosition++];
    data[destPosition++] = image.data[srcPosition++];
    data[destPosition++] = image.data[srcPosition++];
    data[destPosition++] = 255;
  }

  return data;
}

:


const qrCodes = [];
images.forEach((image) => {
  if (image.data) {
    const data = extractRGBAData(image);

    try {
      const code = jsQR(data, image.width, image.height);
      console.log(code);
      qrCodes.push(code);
    } catch (err) {
      console.log(err);
    }
  }
});

7 — QR . URL — QR , . 6 XML ( ):


<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<BarcodeElement xmlns="http://barcodes.pdf.shep.nitec.kz/">
  <creationDate>...</creationDate>
  <elementData>...</elementData>
  <elementNumber>1</elementNumber>
  <elementsAmount>6</elementsAmount>
  <FavorID>...</FavorID>
</BarcodeElement>

:


  • <elementData>...</elementData>
  • <elementNumber>1</elementNumber>
  • <elementsAmount>6</elementsAmount>

3.


:


const qrCodesBlocks = [];

function addQRCodeBlock(code) {
  if (!code || !code.data) {
    return;
  }

  //    .
  const elementsAmountRegexp = /<elementsAmount>((.|\r|\n)+?)<\/elementsAmount>/;
  const elementsAmountResult = elementsAmountRegexp.exec(code.data);
  if (!elementsAmountResult || elementsAmountResult.length <= 2) {
    return;
  }
  const elementsAmount = +elementsAmountResult[1];
  if (!Number.isSafeInteger(elementsAmount)) {
    throw new Error('        <elementsAmount>');
  }

  //       .
  if (qrCodesBlocks.length === 0) {
    for (let i = 0; i < elementsAmount; i++) {
      qrCodesBlocks.push('');
    }
  } else {
    if (qrCodesBlocks.length !== elementsAmount) {
      throw new Error(`  QR      QR : "${qrCodesBlocks.length}"  "${elementsAmount}"`);
    }
  }

  //   .
  const elementNumberRegexp = /<elementNumber>((.|\r|\n)+?)<\/elementNumber>/;
  const elementNumberResult = elementNumberRegexp.exec(code.data);
  if (!elementNumberResult || elementNumberResult.length < 2) {
    throw new Error(` QR   "<elementNumber>"`);
  }
  const elementNumber = +elementNumberResult[1];
  if (!Number.isSafeInteger(elementNumber)) {
    throw new Error(`"<elementNumber>"  QR    `);
  }

  //    .
  if (elementNumber > elementsAmount) {
    throw new Error(` QR  "${elementNumber}"    QR  "${elementsAmount}"`);
  }

  if (qrCodesBlocks[elementNumber - 1] !== '') {
    throw new Error(` QR  "${elementNumber}"    `);
  }

  //     .
  const elementDataRegexp = /<elementData>((.|\r|\n)+?)<\/elementData>/;
  const elementDataResult = elementDataRegexp.exec(code.data);
  if (!elementDataResult || elementDataResult.length < 2) {
    throw new Error(' QR   "<elementData>"');
  }
  const elementData = elementDataResult[1];

  qrCodesBlocks[elementNumber - 1] = elementData;
}

.


qrCodes.forEach(addQRCodeBlock);

if (qrCodesBlocks.length === 0) {
  throw new Error('     QR :     QR    ');
}

const foundBlocks = qrCodesBlocks.filter((block) => !!block);
if (qrCodesBlocks.length !== foundBlocks.length) {
  throw new Error('     QR :      QR ');
}

4.


ZIP Base64.


Base64:


const zippedParts = qrCodesBlocks.map(block => new Uint8Array(gostCrypto.coding.Base64.decode(block)));

:


const totalLength = zippedParts.reduce((accumulator, part) => accumulator + part.length, 0);

const zippedData = new Uint8Array(totalLength);
let zippedDataIndex = 0;
zippedParts.forEach((part) => {
  zippedData.set(part, zippedDataIndex);
  zippedDataIndex += part.length;
});

:


const zip = await JSZip.loadAsync(zippedData, { checkCRC32: true });

one, :


const file = zip.file('one');
if (!file) {
  throw new Error('     "one"');
}

const recoveredContents = await file.async("string");

5.


— XML ( ):


<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<p1001Response>
    <SystemInfo>
        <messageId xsi:nil="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>
        <chainId>...</chainId>
        <messageDate>...</messageDate>
        `1`
        <responseInfoRu> </responseInfoRu>
        <responseInfoKz> </responseInfoKz>
        <digiSign>...</digiSign>
    </SystemInfo>
    <ResponseData>
        <ResponseType>UNJUDGED</ResponseType>
        <Person>
            <IIN>...</IIN>
            <SurName>...</SurName>
            <Name>...</Name>
            <MiddleName>...</MiddleName>
            <BirthDate>...</BirthDate>
            <BirthPlace>
                <Country>...</Country>
                <CountryKz>...</CountryKz>
                <District>...</District>
                <DistrictKz>...</DistrictKz>
                <City>...</City>
                <CityKz>...</CityKz>
                <Locality>...</Locality>
                <LocalityKz>...</LocalityKz>
            </BirthPlace>
        </Person>
        <Untried/>
        <CheckDate>...</CheckDate>
    </ResponseData>
</p1001Response>

digiSign — XML Base64. , .


XML:


const regexp = /<digiSign>((.|\r|\n)+?)<\/digiSign>/;
const regexpResult = regexp.exec(recoveredContents);
if (!regexpResult && regexpResult.length !== 2) {
  throw new Error(' XML  "<digiSign>"');
}

const digiSignBytes = gostCrypto.coding.Base64.decode(regexpResult[1]);
const xmlDataAndSignature = gostCrypto.coding.Chars.encode(digiSignBytes, 'utf8');

6.


XML ( ):


<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<ResponseData>
  <ResponseType>UNJUDGED</ResponseType>
  <Person>
    <IIN>...</IIN>
    <SurName>...</SurName>
    <Name>...</Name>
    <MiddleName>...</MiddleName>
    <BirthDate>...</BirthDate>
    <BirthPlace>
      <Country>...</Country>
      <CountryKz>...</CountryKz>
      <District>...</District>
      <DistrictKz>...</DistrictKz>
      <City>...</City>
      <CityKz>...</CityKz>
      <Locality>...</Locality>
      <LocalityKz>...</LocalityKz>
    </BirthPlace>
  </Person>
  <Untried/>
  <CheckDate>...</CheckDate>
  <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
    <ds:SignedInfo>
      <ds:CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
      <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#gost34310-gost34311"/>
      <ds:Reference URI="">
        <ds:Transforms>
          <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
          <ds:Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"/>
        </ds:Transforms>
        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#gost34311"/>
        <ds:DigestValue>...</ds:DigestValue>
      </ds:Reference>
    </ds:SignedInfo>
    <ds:SignatureValue>...</ds:SignatureValue>
    <ds:KeyInfo>
      <ds:X509Data>
        <ds:X509Certificate>...</ds:X509Certificate>
      </ds:X509Data>
    </ds:KeyInfo>
  </ds:Signature>
</ResponseData>

XML , , <ResponseType>UNJUDGED</ResponseType>, <Person>...</Person>.


7.


XML .


XML :


const xml = XmlDSigJs.Parse(xmlDataAndSignature);

<ds:DigestValue>...</ds:DigestValue>:


const xmlSignatures = XmlDSigJs.Select(xml, "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']");
if (xmlSignatures.length === 0) {
  throw new Error(`      ( "<Signature>"): "${xmlDataAndSignature}"`);
}
if (xmlSignatures.length > 1) {
  throw new Error(`       ( "<Signature>"): "${xmlDataAndSignature}"`);
}

const hashElementsInSignature = XmlDSigJs.Select(xmlSignatures[0], "//*[local-name(.)='DigestValue']");
if (hashElementsInSignature.length === 0) {
  throw new Error(` XML    ( "<DigestValue>"): "${xmlDataAndSignature}"`);
}
if (hashElementsInSignature.length > 1) {
  throw new Error(` XML     ( "<DigestValue>"): "${xmlDataAndSignature}"`);
}
const hashInSignature = hashElementsInSignature[0].textContent;

<ds:Transforms>...</ds:Transforms> XML :


const xmlDsigEnvelopedSignatureTransform = new XmlDSigJs.XmlDsigEnvelopedSignatureTransform();
xmlDsigEnvelopedSignatureTransform.LoadInnerXml(xml.documentElement);
xmlDsigEnvelopedSignatureTransform.GetOutput();

const xmlDsigC14NWithCommentsTransform = new XmlDSigJs.XmlDsigC14NWithCommentsTransform();
xmlDsigC14NWithCommentsTransform.LoadInnerXml(xml.documentElement);
const signedDataXML = xmlDsigC14NWithCommentsTransform.GetOutput();

const dataToHash = gostCrypto.coding.Chars.decode(signedDataXML, 'utf8');

"http://www.w3.org/2001/04/xmldsig-more#gost34311", 34.311-95 GOST R 34.11-94 gostCrypto. D-TEST.


:


const hashBytes = await gostCrypto.subtle.digest({name: 'GOST R 34.11-94', version: 1994, sBox: 'D-TEST'}, dataToHash);
const signedDataXMLHash = gostCrypto.coding.Base64.encode(hashBytes);

:


if (signedDataXMLHash !== hashInSignature) {
  throw new Error(`    XML  "${signedDataXMLHash}"      "${hashInSignature}"`);
}


, . , — , XML .


, : XML , digiSign Base64, HTML , . .


:



All Articles