Análise do conteúdo dos códigos QR em documentos do governo eletrônico da República do Cazaquistão no frontend

Demonstrarei como, usando o JavaScript diretamente no navegador, você pode extrair e analisar dados de códigos QR contidos em documentos gerados por portais eletrônicos do governo da República do Cazaquistão (por exemplo, https://egov.kz ).


Nos documentos eletrônicos, a seguinte redação está presente:


* o código de barras contém dados recebidos do sistema de informações do GDB PH e assinados por uma assinatura digital eletrônica da Filial da NAO da Corporação Estadual “Government for Citizens”.

Até onde eu sei, não existem ferramentas prontas para extrair e analisar dados nos códigos QR.


Meu objetivo final é extrair os dados assinados e a assinatura, verificar a integridade dos dados assinados. Não se fala em verificar uma assinatura digital nesta nota, apenas em verificar um hash. Detalhes da verificação de assinaturas digitais podem ser descritos no futuro, caso o público demonstre interesse neste tópico.


Importante: esta nota não descreve técnicas de hacking e não ajuda a obter acesso não autorizado aos dados; falaremos sobre a conversão de dados de uma visualização para outra.


Mostrarei como processar os arquivos PDF originais que formam e fornecem portais de download do governo eletrônico da República do Cazaquistão. Esses arquivos PDF contêm códigos QR como imagens incorporadas 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