我将演示如何在浏览器中直接使用JavaScript,您可以从哈萨克斯坦共和国电子政府门户网站生成的文档(例如https://egov.kz)中包含的QR码提取和分析数据。
在电子文档中,存在以下措词:
*条形码包含从GDB PH的信息系统接收的数据,并由国家公司“公民政府” NAO的分支机构的电子数字签名签名。
据我所知,还没有现成的工具可以提取和分析QR码中的数据。
我的最终目标是提取签名数据和签名,验证签名数据的完整性。在此说明中,没有谈论验证数字签名的内容,仅讨论了哈希值。万一公众对该主题感兴趣,将来可以描述数字签名验证的详细信息。
重要说明:本说明未描述黑客技术,也无助于获得对数据的未授权访问;我们将讨论将数据从一种视图转换为另一种视图。
我将展示如何处理形成的原始PDF文件并提供哈萨克斯坦共和国电子政府的下载门户。这些PDF文件包含QR码作为单独的嵌入图像。
我将尝试使用没有犯罪记录的证书。
我将使用以下库:
0.在ArrayBuffer中读取PDF文件
使用带有标签<input type="file">
及其属性的标准HTML工具可以访问PDF文件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) {
return image.data;
}
if (image.kind !== 2) {
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 , . .
: