Analisis isi kode QR dalam dokumen pemerintah elektronik Republik Kazakhstan di frontend

Saya akan menunjukkan bagaimana menggunakan JavaScript di browser Anda dapat mengekstraksi dan menganalisis data dari kode QR yang terkandung dalam dokumen yang dihasilkan oleh portal pemerintah elektronik Republik Kazakhstan (misalnya, https://egov.kz ).


Dalam dokumen elektronik, kata-kata berikut hadir:


* barcode berisi data yang diterima dari sistem informasi GDB PH dan ditandatangani dengan tanda tangan digital elektronik dari Cabang Perusahaan Negara β€œGovernment for Citizens” NAO.

Sejauh yang saya tahu, tidak ada alat yang siap pakai untuk mengekstraksi dan menganalisis data dalam kode QR.


Tujuan utama saya adalah untuk mengekstrak data yang ditandatangani dan tanda tangan, memverifikasi integritas data yang ditandatangani. Tidak akan ada pembicaraan untuk memverifikasi tanda tangan digital dalam catatan ini, hanya memverifikasi hash. Detail verifikasi tanda tangan digital dapat dijelaskan di masa depan, jika publik menunjukkan minat pada topik ini.


Penting: catatan ini tidak menjelaskan teknik peretasan dan tidak membantu untuk mendapatkan akses tidak sah ke data, kami akan berbicara tentang mengonversi data dari satu tampilan ke tampilan lainnya.


Saya akan menunjukkan cara memproses file PDF asli yang membentuk dan menyediakan portal pengunduhan dari pemerintah elektronik Republik Kazakhstan. File PDF ini berisi kode QR sebagai gambar tertanam yang terpisah.


.


:



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