mirror of
https://github.com/alam00000/bentopdf.git
synced 2026-11-13 17:54:19 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d33df7968 | ||
|
|
73044e5ea6 | ||
|
|
058053bffc | ||
|
|
4dd34a659a | ||
|
|
25c2d34a9c | ||
|
|
3c7eb141fc | ||
|
|
edd824e1e3 | ||
|
|
18c4aac8bc | ||
|
|
bfc269cb14 | ||
|
|
8e04279c17 | ||
|
|
c244fbe8aa | ||
|
|
2daa7e2ad0 |
No files matched your search
Generated
+151
-104
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "bento-pdf",
|
||||
"version": "2.8.10",
|
||||
"version": "2.8.11",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "bento-pdf",
|
||||
"version": "2.8.10",
|
||||
"version": "2.8.11",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@fontsource/cedarville-cursive": "^5.2.7",
|
||||
@@ -44,7 +44,7 @@
|
||||
"cropperjs": "^1.6.2",
|
||||
"diff": "^8.0.3",
|
||||
"dompurify": "^3.4.17",
|
||||
"fast-xml-parser": "^5.7.1",
|
||||
"fast-xml-parser": "^5.12.0",
|
||||
"heic2any": "^0.0.4",
|
||||
"highlight.js": "^11.12.0",
|
||||
"html2canvas": "^1.4.1",
|
||||
@@ -108,7 +108,7 @@
|
||||
"prettier": "^3.8.1",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.56.1",
|
||||
"vite": "^8.3.5",
|
||||
"vite": "^8.3.6",
|
||||
"vite-plugin-handlebars": "^2.0.0",
|
||||
"vite-plugin-node-polyfills": "^0.26.0",
|
||||
"vitepress": "^1.6.4",
|
||||
@@ -1750,9 +1750,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@nodable/entities": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz",
|
||||
"integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==",
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-3.1.0.tgz",
|
||||
"integrity": "sha512-LsS/DjHr+uDM647Gru/cA8+J3a3HfhttwCKLyuoyN7yXTFCxKBtSgMQBvrW9yNPa2/zDRUNuGPaH5QUFoa4arQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -1762,9 +1762,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@oxc-project/types": {
|
||||
"version": "0.155.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.155.0.tgz",
|
||||
"integrity": "sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==",
|
||||
"version": "0.156.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.156.0.tgz",
|
||||
"integrity": "sha512-1Duyg48TdJNdyhEB8Xn1L36bT9Pv6GUtlKjHxOys8xkaF/rQq3XJE+bAurSK/AgIz5mGFsb77cn9Pb20+WCScw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/oxc-project"
|
||||
@@ -1833,9 +1833,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm-eabi": {
|
||||
"version": "1.2.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.15.tgz",
|
||||
"integrity": "sha512-aLcgK9gIJ3YRvrrtfSBbRa27F5WrpiTaPulnFpsHPxpUHgFlSgnU4854MHwoRGEBhfRJDWCS2WQ7lIjskE9Olw==",
|
||||
"version": "1.2.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.16.tgz",
|
||||
"integrity": "sha512-ia3DwAs0OsMGS7+IConptSiyk9pEXkOx/AbJ2n/lAGxH3QkWQus1t8d7gnAT38FCU2sQu8lUbeR6AiZFv4JxaA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1849,9 +1849,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm64": {
|
||||
"version": "1.2.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.15.tgz",
|
||||
"integrity": "sha512-xD7DdZCrbThKuxNg7ooQmvp07ys4J/pnSEMTG3/OdOWl6daC67m/Q4fL78sRHMxTH/N4fGUbYhcPM0MF5WFHkg==",
|
||||
"version": "1.2.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.16.tgz",
|
||||
"integrity": "sha512-kx5wBiMB6E7TEvpl2B9gjLU004BkvEm2Dr941BFjx/SontPwSzNuQUIoVKxHHoSjpFIfHMmcguyLOtyn0hObRg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1865,9 +1865,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||
"version": "1.2.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.15.tgz",
|
||||
"integrity": "sha512-m2WuQ6r0B4OTYxyPpNLtXaMRbRDUbG8YQV2Q3KTdSSK8Th6KHCnC8jVCJt8bvYeSB5WivkkNbc85mKadaSdOxA==",
|
||||
"version": "1.2.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.16.tgz",
|
||||
"integrity": "sha512-qslZrXnKJU4cUpjoAmtAo/KQM9e2IRMlmpig6Zpwj+8KWxW8v+nkAf9216Bsek9lOC1jvg2yCjygCFo2f83msQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1881,9 +1881,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-x64": {
|
||||
"version": "1.2.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.15.tgz",
|
||||
"integrity": "sha512-5RtnsHM36nodM9WbMy+zip0LGS/G+y5wro5RHrpOlr4ac24+1zSi9kiV2MP1myZeZ/ifEHfulQcWjnAXzc2C2A==",
|
||||
"version": "1.2.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.16.tgz",
|
||||
"integrity": "sha512-FdF+y48ND13eAyM2mQn+85kvAqNlvRWROy2rhgA463X/0Gv4vGtazZtfaF1mRBLscYvtOlh4QnLjOWiFzX69OQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1897,9 +1897,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||
"version": "1.2.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.15.tgz",
|
||||
"integrity": "sha512-g3l09pyFqhu+135JKkbkwiKGQmwU5hUMUPJ4VdBADvuqR8d33L6H578rh8m7rtxbdp+VmX/+nzI7nvhSZlzpUQ==",
|
||||
"version": "1.2.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.16.tgz",
|
||||
"integrity": "sha512-ddjqFM5e95AgLbUPtKIS7V1KTVlA0RUgCVr5mn+/IEhsdy3pr+Vuj/HZ7nlMN2FyxIwUktOH1LEPIQXO1v9y1Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1913,9 +1913,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||
"version": "1.2.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.15.tgz",
|
||||
"integrity": "sha512-nIe3VugI97nGQtspPxDqA4sXMUpeiocKIP/CqpY/u/vgbT+alX+cVxu1C/oVBPKg4feNTmYovE91v4DgXvXNwQ==",
|
||||
"version": "1.2.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.16.tgz",
|
||||
"integrity": "sha512-tHdKWbFkFBI687iim3LIyIvyL7j66VvQYkDPss5Stn/qyE2tWkR1YD0dXmXPFKKqT/o5ZEJVsaRsWS2meQwGOA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1929,12 +1929,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||
"version": "1.2.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.15.tgz",
|
||||
"integrity": "sha512-iTBjXx+B5QSvgI89lJfmqh4wTh8NabR1YZIIN3hNx4zSG0EVfVqkOvLulNpG4251pbd6/IAmZ/LefVk1Y4rPhw==",
|
||||
"version": "1.2.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.16.tgz",
|
||||
"integrity": "sha512-FJiLGAblPBMlBEdbOqw6kW6QcXDWAf/oJu7gYs9ZJZwmg+K+/zjFLxPTIY9izJUzxzc24FHvmseyA0IvJSl17w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1945,12 +1948,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||
"version": "1.2.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.15.tgz",
|
||||
"integrity": "sha512-uN26XOnVR7rqQupG3V9oAG2EGGatWjcJHOeDOHUFCh4TBWmP2tHTFoVN13aYbwfm0aHqJAEmcwhAu6sooIjfqQ==",
|
||||
"version": "1.2.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.16.tgz",
|
||||
"integrity": "sha512-hrJpblMsrQ58U9veWpbXzkhV5zStunds6HWLprqq1qauvgBr1E6P+24kLj6lSLI9vjRn/RYpjprhkHQPKGWpNg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1961,12 +1967,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||
"version": "1.2.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.15.tgz",
|
||||
"integrity": "sha512-9nWkcdd+H+XhaA3fQIwpHnKIrfx/PQ6rO0pivr78Og07tXlUlctBBJoEYLPfhTmCR/QxEVdn6uPjHV5SymPhdw==",
|
||||
"version": "1.2.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.16.tgz",
|
||||
"integrity": "sha512-qJd/dC6j6CwLG9/s/RP2nb1RZ0GIgj7Ioj7x/SYa3RzjVrlzrmH0fhIOpfvqQDeIsdP0loi+kHaMhJzY1PBIaA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1977,12 +1986,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||
"version": "1.2.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.15.tgz",
|
||||
"integrity": "sha512-Ggh0ciFjLwIusLSLz7LZYEaGdxOAUJXI+Qu5JCbhPJJHEt8vP+IjRATTJheiJsdzzTYgUiHUiw3X1Ea3ZpMBdA==",
|
||||
"version": "1.2.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.16.tgz",
|
||||
"integrity": "sha512-Vx7Xg5uUJuNcujJjI70OICaH0EYj80BtOLZTSN7uOYg8QOLlyKnaG0IW7Fn/J0IpJsaKzmDKMhOQv1JBM9m9vg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1993,12 +2005,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||
"version": "1.2.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.15.tgz",
|
||||
"integrity": "sha512-FHg60rS6DBePAYZ9X9F67x+JObFdmgn6tupA1C/a3B+CufFmTD4mX+e8M75tCiAO3foR/6xwVBI0KQiI7sn/rw==",
|
||||
"version": "1.2.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.16.tgz",
|
||||
"integrity": "sha512-TNKhgE3uJTsTzatUSSYt0Kgo+Oa2uSlSjz4r1aEIea7HX6ULBym4fAlYUhG1i6pjNf2njfCUAfKwr6C3E9Xg1w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2009,12 +2024,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||
"version": "1.2.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.15.tgz",
|
||||
"integrity": "sha512-/cvV/7v5VGz/moiz0XKRj2dE8DzsKyhIu1KpOvJGPjRywg3yscON889UGvAe8zfiJLHup7VLjBduoGWi3HusWQ==",
|
||||
"version": "1.2.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.16.tgz",
|
||||
"integrity": "sha512-7chXdpjXZ8qG0+mIxECN3cT9kvhZ3/oZhMX6Pp876P7CDrIy8nJswva6Mq357JR7eYXfuskwZxMeV1SzoZdHrA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2025,9 +2043,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||
"version": "1.2.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.15.tgz",
|
||||
"integrity": "sha512-MyzC5Hrv/sm+Z7TSGKP/j8001RO2xQm1zOpCNVL4j0cY/VhBZviludA2YFQ7i4lH6ZAXxQm0lcMdBzM+99mE7w==",
|
||||
"version": "1.2.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.16.tgz",
|
||||
"integrity": "sha512-Kwjt+7uNTJK5/MDkGWGVO2KQPnj8yAV75dMpfwyHZrJSXWHrniOB0aUEWlVYf3H1hOqFLIStLdM5QXq6bnKmOQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2041,9 +2059,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||
"version": "1.2.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.15.tgz",
|
||||
"integrity": "sha512-+PgYiS+AH5omy6L45ZLxybasauJaj3M8buoT83D+nzPmCayXlxUV9hydhciCKVyIg0B/v0BtXPDE1uGqNwCPpg==",
|
||||
"version": "1.2.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.16.tgz",
|
||||
"integrity": "sha512-/LH6WfjDRXXJz/p5xl4TmU+jpcnwcxbKEQBW/GZE0JHfN0lGIyTtsUEGaQ/q/GikkGJ2yLKe6pwm7AnEWO8pdw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2057,9 +2075,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||
"version": "1.2.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.15.tgz",
|
||||
"integrity": "sha512-HFZd2QNQYn1JbIi+Hpd7H+r/lkWxcfIu28nirZ6eCn2LafuvmExMlb/P96Sfx5tf7pPArw39IdhmCF8vmV+NhQ==",
|
||||
"version": "1.2.16",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.16.tgz",
|
||||
"integrity": "sha512-1lv1KvX0Z871zfk7vw+dnXMMnHNyDwMzWG8Kv3HxDXMOhzCX4SHc7HK319fInP0i+0p9D8brkfzL7pJOaGl+tw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -4242,6 +4260,18 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/anynum": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz",
|
||||
"integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/archiver": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz",
|
||||
@@ -6576,9 +6606,9 @@
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/fast-xml-builder": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz",
|
||||
"integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.3.1.tgz",
|
||||
"integrity": "sha512-pIM/1n3ntFXKYrUZwW7QCK0gAW7XY+wzj1YMIV3tLDvPj/V+zTGJK5e3/4WJfwj0qWw2ElNXiTixda/R+3YSug==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -6587,14 +6617,14 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-expression-matcher": "^1.5.0",
|
||||
"xml-naming": "^0.1.0"
|
||||
"path-expression-matcher": "^1.6.2",
|
||||
"xml-naming": "^0.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-xml-parser": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.1.tgz",
|
||||
"integrity": "sha512-8Cc3f8GUGUULg34pBch/KGyPLglS+OFs05deyOlY7fL2MTagYPKrVQNmR1fLF/yJ9PH5ZSTd3YDF6pnmeZU+zA==",
|
||||
"version": "5.12.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.12.0.tgz",
|
||||
"integrity": "sha512-/pXxRBtwxY9w8z7YbgsvIPPcPvFDsTbIkWwTQJhzNJPokj+TbN6ftmcv4j/pEQDvOCNnKGDzuk5Bo2oogIOiFg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -6603,10 +6633,12 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodable/entities": "^2.1.0",
|
||||
"fast-xml-builder": "^1.1.5",
|
||||
"path-expression-matcher": "^1.5.0",
|
||||
"strnum": "^2.2.3"
|
||||
"@nodable/entities": "^3.1.0",
|
||||
"fast-xml-builder": "^1.3.1",
|
||||
"is-unsafe": "^2.0.3",
|
||||
"path-expression-matcher": "^1.6.2",
|
||||
"strnum": "^2.5.1",
|
||||
"xml-naming": "^0.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"fxparser": "src/cli/cli.js"
|
||||
@@ -7582,6 +7614,18 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-unsafe": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.3.tgz",
|
||||
"integrity": "sha512-ZBllczCCO267vgdFb6Tq3pv+LhHaCiGMdr+ZsuL+OopruAg7S+3ZDs1vD0RPC0uSrkjzbraxaGjtX1YBxjPeFQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-url": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz",
|
||||
@@ -9402,9 +9446,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/path-expression-matcher": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz",
|
||||
"integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==",
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz",
|
||||
"integrity": "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -9640,9 +9684,9 @@
|
||||
"license": "MIT-0"
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.31",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz",
|
||||
"integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==",
|
||||
"version": "8.5.32",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.32.tgz",
|
||||
"integrity": "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -10183,12 +10227,12 @@
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/rolldown": {
|
||||
"version": "1.2.15",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.15.tgz",
|
||||
"integrity": "sha512-6odND+fel5zVw3I4Yc2e69gvguDnX0CAAGiNDZSq4ik5WUcaLrkD2GLuog0XcODbor31hZ5ayEMgVDno+KPV5g==",
|
||||
"version": "1.2.16",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.16.tgz",
|
||||
"integrity": "sha512-bWvlMpFKiTfDKfnK7FC2MPgPeWCf/Kpwsq49ApdW+FQaBZb6O6dZ4MAPbPUjp7hYQrw/RobqCzn64BRrQecGHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oxc-project/types": "=0.155.0",
|
||||
"@oxc-project/types": "=0.156.0",
|
||||
"@rolldown/pluginutils": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
@@ -10198,21 +10242,21 @@
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rolldown/binding-android-arm-eabi": "1.2.15",
|
||||
"@rolldown/binding-android-arm64": "1.2.15",
|
||||
"@rolldown/binding-darwin-arm64": "1.2.15",
|
||||
"@rolldown/binding-darwin-x64": "1.2.15",
|
||||
"@rolldown/binding-freebsd-x64": "1.2.15",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.2.15",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.2.15",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.2.15",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.2.15",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.2.15",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.2.15",
|
||||
"@rolldown/binding-linux-x64-musl": "1.2.15",
|
||||
"@rolldown/binding-openharmony-arm64": "1.2.15",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.2.15",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.2.15"
|
||||
"@rolldown/binding-android-arm-eabi": "1.2.16",
|
||||
"@rolldown/binding-android-arm64": "1.2.16",
|
||||
"@rolldown/binding-darwin-arm64": "1.2.16",
|
||||
"@rolldown/binding-darwin-x64": "1.2.16",
|
||||
"@rolldown/binding-freebsd-x64": "1.2.16",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.2.16",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.2.16",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.2.16",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.2.16",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.2.16",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.2.16",
|
||||
"@rolldown/binding-linux-x64-musl": "1.2.16",
|
||||
"@rolldown/binding-openharmony-arm64": "1.2.16",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.2.16",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.2.16"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
@@ -10867,16 +10911,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/strnum": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz",
|
||||
"integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.5.1.tgz",
|
||||
"integrity": "sha512-UM4nTLWzQiDBseVxUGxN0wJzIk0UU89Qi7pKXz9qISo+lhviJikTuikBsK/oFRlpvYHvSdz0TVoLyQ+sEpYwxA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"anynum": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/stylis": {
|
||||
"version": "4.3.6",
|
||||
@@ -11589,15 +11636,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.3.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.5.tgz",
|
||||
"integrity": "sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==",
|
||||
"version": "8.3.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.6.tgz",
|
||||
"integrity": "sha512-UI7dK29NnS54Fk+mmo7o+jI6U8Fp7hDl+X5Q1k45WH+d4dSlbLVUETGQ+hfkNgvLQGyoElPj837nZyv4MBc0VA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.33.0",
|
||||
"picomatch": "^4.0.7",
|
||||
"postcss": "^8.5.30",
|
||||
"rolldown": "~1.2.14",
|
||||
"postcss": "^8.5.32",
|
||||
"rolldown": "~1.2.16",
|
||||
"tinyglobby": "^0.2.17"
|
||||
},
|
||||
"bin": {
|
||||
@@ -12945,9 +12992,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/xml-naming": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz",
|
||||
"integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==",
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz",
|
||||
"integrity": "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
|
||||
+2
-2
@@ -65,7 +65,7 @@
|
||||
"prettier": "^3.8.1",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.56.1",
|
||||
"vite": "^8.3.5",
|
||||
"vite": "^8.3.6",
|
||||
"vite-plugin-handlebars": "^2.0.0",
|
||||
"vite-plugin-node-polyfills": "^0.26.0",
|
||||
"vitepress": "^1.6.4",
|
||||
@@ -108,7 +108,7 @@
|
||||
"cropperjs": "^1.6.2",
|
||||
"diff": "^8.0.3",
|
||||
"dompurify": "^3.4.17",
|
||||
"fast-xml-parser": "^5.7.1",
|
||||
"fast-xml-parser": "^5.12.0",
|
||||
"heic2any": "^0.0.4",
|
||||
"highlight.js": "^11.12.0",
|
||||
"html2canvas": "^1.4.1",
|
||||
|
||||
+11
-7
@@ -61,6 +61,14 @@ export const languageNames: Record<SupportedLanguage, string> = {
|
||||
fa: 'فارسی',
|
||||
};
|
||||
|
||||
// Longest codes first so multi-part tags like zh-TW win over their prefix (zh).
|
||||
const LANGUAGE_PATTERN = [...supportedLanguages]
|
||||
.sort((a, b) => b.length - a.length)
|
||||
.join('|');
|
||||
|
||||
const URL_LANGUAGE_REGEX = new RegExp(`^/(${LANGUAGE_PATTERN})(?:/|$)`);
|
||||
const LANGUAGE_PREFIX_REGEX = new RegExp(`^/(${LANGUAGE_PATTERN})(/.*)?$`);
|
||||
|
||||
export const getLanguageFromUrl = (): SupportedLanguage => {
|
||||
const basePath = import.meta.env.BASE_URL.replace(/\/$/, '');
|
||||
let path = window.location.pathname;
|
||||
@@ -73,9 +81,7 @@ export const getLanguageFromUrl = (): SupportedLanguage => {
|
||||
path = '/' + path;
|
||||
}
|
||||
|
||||
const langMatch = path.match(
|
||||
/^\/(en|ar|fr|es|de|zh|zh-TW|vi|tr|id|it|pt|nl|be|da|ko|sv|ru|ja|uk|sk|ca|fa)(?:\/|$)/
|
||||
);
|
||||
const langMatch = path.match(URL_LANGUAGE_REGEX);
|
||||
if (
|
||||
langMatch &&
|
||||
supportedLanguages.includes(langMatch[1] as SupportedLanguage)
|
||||
@@ -211,9 +217,7 @@ export const changeLanguage = (lang: SupportedLanguage): void => {
|
||||
}
|
||||
|
||||
let pagePathWithoutLang = relativePath;
|
||||
const langPrefixMatch = relativePath.match(
|
||||
/^\/(en|ar|fr|es|de|zh|zh-TW|vi|tr|id|it|pt|nl|be|da|ko|sv|ru|ja|uk|sk|ca|fa)(\/.*)?$/
|
||||
);
|
||||
const langPrefixMatch = relativePath.match(LANGUAGE_PREFIX_REGEX);
|
||||
if (langPrefixMatch) {
|
||||
pagePathWithoutLang = langPrefixMatch[2] || '/';
|
||||
}
|
||||
@@ -320,7 +324,7 @@ export const rewriteLinks = (): void => {
|
||||
}
|
||||
|
||||
const langPrefixRegex = new RegExp(
|
||||
`^(${basePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})?/?(en|ar|fr|es|de|zh|zh-TW|vi|tr|id|it|pt|nl|be|da|ko|sv|ru|ja|uk|sk|ca|fa)(/|$|[?#])`
|
||||
`^(${basePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})?/?(${LANGUAGE_PATTERN})(/|$|[?#])`
|
||||
);
|
||||
if (langPrefixRegex.test(href)) {
|
||||
return;
|
||||
|
||||
@@ -367,7 +367,10 @@ function createSignatureCard(
|
||||
|
||||
let statusColor = 'text-green-400';
|
||||
let statusIcon = 'check-circle';
|
||||
let statusText = trText('Valid Signature');
|
||||
const isTimestamp = result.isTimestamp === true;
|
||||
let statusText = isTimestamp
|
||||
? trText('Valid Timestamp')
|
||||
: trText('Valid Signature');
|
||||
|
||||
if (!result.isValid) {
|
||||
if (result.cryptoVerificationStatus === 'unsupported') {
|
||||
@@ -380,19 +383,19 @@ function createSignatureCard(
|
||||
) {
|
||||
statusColor = 'text-yellow-400';
|
||||
statusIcon = 'alert-triangle';
|
||||
statusText = trText('Modified After Signing (signed part intact)');
|
||||
statusText = isTimestamp
|
||||
? trText('Modified After Timestamping (timestamped part intact)')
|
||||
: trText('Modified After Signing (signed part intact)');
|
||||
} else {
|
||||
statusColor = 'text-red-400';
|
||||
statusIcon = 'x-circle';
|
||||
statusText =
|
||||
result.cryptoVerified === false
|
||||
? trText('Invalid: Cryptographic Verification Failed')
|
||||
: trText('Invalid Signature');
|
||||
: result.usesInsecureDigest
|
||||
? trText('Insecure Digest (MD5 / SHA-1)')
|
||||
: trText('Invalid Signature');
|
||||
}
|
||||
} else if (result.usesInsecureDigest) {
|
||||
statusColor = 'text-red-400';
|
||||
statusIcon = 'x-circle';
|
||||
statusText = trText('Insecure Digest (MD5 / SHA-1)');
|
||||
} else if (result.isExpired) {
|
||||
statusColor = 'text-yellow-400';
|
||||
statusIcon = 'alert-triangle';
|
||||
@@ -400,9 +403,11 @@ function createSignatureCard(
|
||||
} else if (!result.isTrusted) {
|
||||
statusColor = 'text-yellow-400';
|
||||
statusIcon = 'alert-triangle';
|
||||
statusText = result.isSelfSigned
|
||||
? trText('Self-Signed: Signer Identity Not Verified')
|
||||
: trText('Signature Intact: Signer Identity Not Verified');
|
||||
statusText = isTimestamp
|
||||
? trText('Timestamp Intact: Timestamp Authority Not Verified')
|
||||
: result.isSelfSigned
|
||||
? trText('Self-Signed: Signer Identity Not Verified')
|
||||
: trText('Signature Intact: Signer Identity Not Verified');
|
||||
}
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
@@ -430,7 +435,7 @@ function createSignatureCard(
|
||||
<div class="flex items-center gap-3">
|
||||
<i data-lucide="${statusIcon}" class="w-6 h-6 ${statusColor}"></i>
|
||||
<div>
|
||||
<h3 class="font-semibold text-white">${trHtml('Signature {{n}}', { n: index + 1 })}</h3>
|
||||
<h3 class="font-semibold text-white">${isTimestamp ? trHtml('Document Timestamp {{n}}', { n: index + 1 }) : trHtml('Signature {{n}}', { n: index + 1 })}</h3>
|
||||
<p class="text-sm ${statusColor}">${statusText}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -448,7 +453,7 @@ function createSignatureCard(
|
||||
<div class="space-y-3 text-sm">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p class="text-gray-400">${trHtml('Signed By')}</p>
|
||||
<p class="text-gray-400">${isTimestamp ? trHtml('Timestamped By') : trHtml('Signed By')}</p>
|
||||
<p class="text-white font-medium">${escapeHtml(result.signerName)}</p>
|
||||
${result.signerOrg ? `<p class="text-gray-400 text-xs">${escapeHtml(result.signerOrg)}</p>` : ''}
|
||||
${result.signerEmail ? `<p class="text-gray-400 text-xs">${escapeHtml(result.signerEmail)}</p>` : ''}
|
||||
@@ -464,7 +469,7 @@ function createSignatureCard(
|
||||
result.signatureDate
|
||||
? `
|
||||
<div>
|
||||
<p class="text-gray-400">${trHtml('Signed On')}</p>
|
||||
<p class="text-gray-400">${isTimestamp ? trHtml('Timestamped On') : trHtml('Signed On')}</p>
|
||||
<p class="text-white">${formatDate(result.signatureDate)}</p>
|
||||
</div>
|
||||
`
|
||||
@@ -512,6 +517,7 @@ function createSignatureCard(
|
||||
<p><span class="text-gray-400">${trHtml('Serial Number:')}</span> <span class="text-gray-300 font-mono">${escapeHtml(result.serialNumber)}</span></p>
|
||||
<p><span class="text-gray-400">${trHtml('Digest Algorithm:')}</span> <span class="text-gray-300">${escapeHtml(result.algorithms.digest)}</span></p>
|
||||
<p><span class="text-gray-400">${trHtml('Signature Algorithm:')}</span> <span class="text-gray-300">${escapeHtml(result.algorithms.signature)}</span></p>
|
||||
${result.algorithms.imprint ? `<p><span class="text-gray-400">${trHtml('Timestamp Imprint Algorithm:')}</span> <span class="text-gray-300">${escapeHtml(result.algorithms.imprint)}</span></p>` : ''}
|
||||
${result.errorMessage ? `<p class="text-red-400">${trHtml('Error: {{error}}', { error: result.errorMessage })}</p>` : ''}
|
||||
${result.unsupportedAlgorithmReason ? `<p class="text-yellow-300">${escapeHtml(result.unsupportedAlgorithmReason)}</p>` : ''}
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,8 @@ const INSECURE_DIGEST_OIDS = new Set<string>([
|
||||
|
||||
const MIN_RSA_EXPONENT = new forge.jsbn.BigInteger('65537');
|
||||
|
||||
const TST_INFO_OID = '1.2.840.113549.1.9.16.1.4';
|
||||
|
||||
const PDF_WHITESPACE = new Set('\0\t\n\f\r ');
|
||||
const PDF_TOKEN_TERMINATORS = new Set('\0\t\n\f\r ()<>[]{}/%');
|
||||
const PDF_STRING_ESCAPES: Record<string, string> = {
|
||||
@@ -52,13 +54,15 @@ export function extractSignatures(
|
||||
if (end < 0) continue;
|
||||
|
||||
for (const entries of dictionaries) {
|
||||
if (isSignatureDictionary(pdfString, entries)) {
|
||||
const kind = getSignatureKind(pdfString, entries);
|
||||
if (kind) {
|
||||
signatures.push(
|
||||
readSignatureDictionary(
|
||||
pdfString,
|
||||
pdfBytes,
|
||||
entries,
|
||||
signatures.length
|
||||
signatures.length,
|
||||
kind === 'timestamp'
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -72,33 +76,36 @@ export function extractSignatures(
|
||||
* /Type is optional in a signature dictionary (PDF 32000-1, 12.8.1, Table
|
||||
* 252), and some signers leave it out, so a dictionary without /Type that
|
||||
* carries both /ByteRange and /Contents is a signature too. Document
|
||||
* timestamps (/Type /DocTimeStamp, /SubFilter /ETSI.RFC3161) hold an RFC 3161
|
||||
* token rather than a CMS signature over the document and stay out.
|
||||
* timestamps (/Type /DocTimeStamp, /SubFilter /ETSI.RFC3161, ISO 32000-2,
|
||||
* 12.8.5) hold an RFC 3161 token instead of a CMS signature over the
|
||||
* document, so they are reported separately and verified differently.
|
||||
*/
|
||||
function isSignatureDictionary(
|
||||
function getSignatureKind(
|
||||
pdfString: string,
|
||||
entries: Map<string, PdfValueSpan>
|
||||
): boolean {
|
||||
): 'signature' | 'timestamp' | null {
|
||||
const name = (key: string) => {
|
||||
const span = entries.get(key);
|
||||
return span ? pdfString.slice(span.start, span.end) : undefined;
|
||||
};
|
||||
const type = name('Type');
|
||||
if (type !== undefined) return type === '/Sig';
|
||||
return (
|
||||
entries.has('ByteRange') &&
|
||||
entries.has('Contents') &&
|
||||
name('SubFilter') !== '/ETSI.RFC3161'
|
||||
);
|
||||
if (type === '/DocTimeStamp') return 'timestamp';
|
||||
if (type !== undefined) return type === '/Sig' ? 'signature' : null;
|
||||
if (!entries.has('ByteRange') || !entries.has('Contents')) return null;
|
||||
return name('SubFilter') === '/ETSI.RFC3161' ? 'timestamp' : 'signature';
|
||||
}
|
||||
|
||||
function readSignatureDictionary(
|
||||
pdfString: string,
|
||||
pdfBytes: Uint8Array,
|
||||
entries: Map<string, PdfValueSpan>,
|
||||
index: number
|
||||
index: number,
|
||||
isTimestamp: boolean
|
||||
): ExtractedSignatureWithError {
|
||||
const text = (key: string) => decodePdfText(pdfBytes, entries.get(key));
|
||||
// A PDF processor should ignore these on ETSI.RFC3161 dictionaries (ISO
|
||||
// 32000-2, Table 255)
|
||||
const signerText = (key: string) => (isTimestamp ? undefined : text(key));
|
||||
const timeMatch = text('M')?.match(
|
||||
/^D:(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/
|
||||
);
|
||||
@@ -106,13 +113,14 @@ function readSignatureDictionary(
|
||||
index,
|
||||
contents: new Uint8Array(0),
|
||||
byteRange: [],
|
||||
reason: text('Reason'),
|
||||
location: text('Location'),
|
||||
contactInfo: text('ContactInfo'),
|
||||
name: text('Name'),
|
||||
reason: signerText('Reason'),
|
||||
location: signerText('Location'),
|
||||
contactInfo: signerText('ContactInfo'),
|
||||
name: signerText('Name'),
|
||||
signingTime: timeMatch
|
||||
? `${timeMatch[1]}-${timeMatch[2]}-${timeMatch[3]}T${timeMatch[4]}:${timeMatch[5]}:${timeMatch[6]}`
|
||||
: undefined,
|
||||
isTimestamp,
|
||||
};
|
||||
|
||||
const rangeSpan = entries.get('ByteRange');
|
||||
@@ -342,6 +350,7 @@ export async function validateSignature(
|
||||
reason: signature.reason,
|
||||
location: signature.location,
|
||||
contactInfo: signature.contactInfo,
|
||||
isTimestamp: signature.isTimestamp,
|
||||
};
|
||||
|
||||
if (signature.extractionError) {
|
||||
@@ -357,6 +366,13 @@ export async function validateSignature(
|
||||
Array.from(signature.contents)
|
||||
);
|
||||
const asn1 = forge.asn1.fromDer(binaryString);
|
||||
const timestamp = signature.isTimestamp ? unwrapTimestampToken(asn1) : null;
|
||||
if (signature.isTimestamp && !timestamp) {
|
||||
result.errorMessage = trText('Could not read the timestamp token');
|
||||
result.cryptoVerified = false;
|
||||
result.cryptoVerificationStatus = 'failed';
|
||||
return result;
|
||||
}
|
||||
const p7 = forge.pkcs7.messageFromAsn1(
|
||||
asn1
|
||||
) as forge.pkcs7.PkcsSignedData & {
|
||||
@@ -429,14 +445,22 @@ export async function validateSignature(
|
||||
(digestOid && getDigestAlgorithmName(digestOid)) ||
|
||||
getDigestAlgorithmName(signerCert.siginfo?.algorithmOid || ''),
|
||||
signature: getSignatureAlgorithmName(signerCert.signatureOid || ''),
|
||||
imprint: timestamp
|
||||
? getDigestAlgorithmName(timestamp.imprintOid)
|
||||
: undefined,
|
||||
};
|
||||
|
||||
if (digestOid && INSECURE_DIGEST_OIDS.has(digestOid)) {
|
||||
result.usesInsecureDigest = true;
|
||||
}
|
||||
if (timestamp && INSECURE_DIGEST_OIDS.has(timestamp.imprintOid)) {
|
||||
result.usesInsecureDigest = true;
|
||||
}
|
||||
|
||||
// Parse signing time if available in signature
|
||||
if (signature.signingTime) {
|
||||
if (timestamp?.genTime) {
|
||||
result.signatureDate = timestamp.genTime;
|
||||
} else if (signature.signingTime) {
|
||||
result.signatureDate = new Date(signature.signingTime);
|
||||
} else {
|
||||
try {
|
||||
@@ -490,7 +514,8 @@ export async function validateSignature(
|
||||
pdfBytes,
|
||||
signature.byteRange,
|
||||
signerCert,
|
||||
signerInfoFields
|
||||
signerInfoFields,
|
||||
timestamp
|
||||
);
|
||||
|
||||
result.cryptoVerified = verification.status === 'verified';
|
||||
@@ -640,6 +665,80 @@ interface SignerInfoFields {
|
||||
signatureBytes: string;
|
||||
}
|
||||
|
||||
interface TimestampToken {
|
||||
tstInfo: Uint8Array;
|
||||
imprintOid: string;
|
||||
imprint: string;
|
||||
genTime?: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the TSTInfo out of an RFC 3161 timestamp token and relabels its
|
||||
* encapsulated content as id-data, because node-forge only parses SignedData
|
||||
* that wraps plain data.
|
||||
*/
|
||||
function unwrapTimestampToken(
|
||||
contentInfo: forge.asn1.Asn1
|
||||
): TimestampToken | null {
|
||||
try {
|
||||
const signedData = asn1Children(asn1Children(contentInfo)?.[1])?.[0];
|
||||
const encapContentInfo = asn1Children(signedData)?.[2];
|
||||
const [typeNode, explicitContent] = asn1Children(encapContentInfo) ?? [];
|
||||
if (
|
||||
typeNode?.type !== forge.asn1.Type.OID ||
|
||||
forge.asn1.derToOid(typeNode.value as string) !== TST_INFO_OID
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const octets = asn1Children(explicitContent)?.[0];
|
||||
if (!octets || octets.type !== forge.asn1.Type.OCTETSTRING) return null;
|
||||
const tstInfoDer =
|
||||
typeof octets.value === 'string'
|
||||
? octets.value
|
||||
: (octets.value as forge.asn1.Asn1[])
|
||||
.map((chunk) => chunk.value as string)
|
||||
.join('');
|
||||
|
||||
const tstInfo = asn1Children(forge.asn1.fromDer(tstInfoDer));
|
||||
const [hashAlgorithm, hashedMessage] = asn1Children(tstInfo?.[2]) ?? [];
|
||||
const imprintOidNode = asn1Children(hashAlgorithm)?.[0];
|
||||
if (
|
||||
imprintOidNode?.type !== forge.asn1.Type.OID ||
|
||||
hashedMessage?.type !== forge.asn1.Type.OCTETSTRING ||
|
||||
typeof hashedMessage.value !== 'string'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let genTime: Date | undefined;
|
||||
const genTimeNode = tstInfo?.[4];
|
||||
if (
|
||||
genTimeNode?.type === forge.asn1.Type.GENERALIZEDTIME &&
|
||||
typeof genTimeNode.value === 'string'
|
||||
) {
|
||||
genTime = forge.asn1.generalizedTimeToDate(genTimeNode.value);
|
||||
}
|
||||
|
||||
typeNode.value = forge.asn1.oidToDer(forge.pki.oids.data).getBytes();
|
||||
|
||||
return {
|
||||
tstInfo: latin1ToUint8(tstInfoDer),
|
||||
imprintOid: forge.asn1.derToOid(imprintOidNode.value as string),
|
||||
imprint: hashedMessage.value,
|
||||
genTime,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function asn1Children(
|
||||
node: forge.asn1.Asn1 | undefined
|
||||
): forge.asn1.Asn1[] | null {
|
||||
return node && Array.isArray(node.value) ? node.value : null;
|
||||
}
|
||||
|
||||
function extractSignerInfoFields(
|
||||
p7: forge.pkcs7.PkcsSignedData & {
|
||||
rawCapture?: {
|
||||
@@ -1019,7 +1118,8 @@ async function performCryptoVerification(
|
||||
pdfBytes: Uint8Array,
|
||||
byteRange: number[],
|
||||
signerCert: forge.pki.Certificate,
|
||||
fields: SignerInfoFields | null
|
||||
fields: SignerInfoFields | null,
|
||||
timestamp: TimestampToken | null = null
|
||||
): Promise<CryptoVerificationResult> {
|
||||
if (!fields) {
|
||||
return { status: 'failed', reason: trText('Could not parse signer info') };
|
||||
@@ -1044,7 +1144,33 @@ async function performCryptoVerification(
|
||||
signedContent.set(pdfBytes.subarray(start1, start1 + len1), 0);
|
||||
signedContent.set(pdfBytes.subarray(start2, start2 + len2), len1);
|
||||
|
||||
md.update(uint8ToLatin1(signedContent));
|
||||
// A timestamp token signs its TSTInfo, which in turn holds the hash of the
|
||||
// document bytes (RFC 3161, 2.4.2), so the document is checked against that
|
||||
// imprint and the CMS signature against the TSTInfo.
|
||||
let content: Uint8Array = signedContent;
|
||||
if (timestamp) {
|
||||
const imprintMd = createMd(timestamp.imprintOid);
|
||||
if (!imprintMd) {
|
||||
return {
|
||||
status: 'unsupported',
|
||||
reason: trText('Unsupported digest OID {{digestOid}}', {
|
||||
digestOid: timestamp.imprintOid,
|
||||
}),
|
||||
};
|
||||
}
|
||||
imprintMd.update(uint8ToLatin1(signedContent));
|
||||
if (imprintMd.digest().bytes() !== timestamp.imprint) {
|
||||
return {
|
||||
status: 'failed',
|
||||
reason: trText(
|
||||
'Document hash does not match the timestamp: PDF was modified after timestamping'
|
||||
),
|
||||
};
|
||||
}
|
||||
content = timestamp.tstInfo;
|
||||
}
|
||||
|
||||
md.update(uint8ToLatin1(content));
|
||||
const contentHashBytes = md.digest().bytes();
|
||||
|
||||
const authAttrs = fields.authAttrs;
|
||||
@@ -1107,7 +1233,7 @@ async function performCryptoVerification(
|
||||
);
|
||||
signedBytesForVerify = latin1ToUint8(forge.asn1.toDer(asSet).getBytes());
|
||||
} else {
|
||||
signedBytesForVerify = signedContent;
|
||||
signedBytesForVerify = content;
|
||||
}
|
||||
|
||||
if (scheme.kind === 'rsa-pkcs1' && !hasLowRsaExponent(signerCert.publicKey)) {
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface SignatureValidationResult {
|
||||
algorithms: {
|
||||
digest: string;
|
||||
signature: string;
|
||||
imprint?: string;
|
||||
};
|
||||
serialNumber: string;
|
||||
reason?: string;
|
||||
@@ -29,6 +30,7 @@ export interface SignatureValidationResult {
|
||||
cryptoVerificationStatus?: 'verified' | 'failed' | 'unsupported';
|
||||
unsupportedAlgorithmReason?: string;
|
||||
usesInsecureDigest?: boolean;
|
||||
isTimestamp?: boolean;
|
||||
}
|
||||
|
||||
export interface ExtractedSignature {
|
||||
@@ -40,6 +42,7 @@ export interface ExtractedSignature {
|
||||
contactInfo?: string;
|
||||
name?: string;
|
||||
signingTime?: string;
|
||||
isTimestamp?: boolean;
|
||||
}
|
||||
|
||||
export interface ValidateSignatureState {
|
||||
|
||||
@@ -136,6 +136,8 @@
|
||||
>
|
||||
<button
|
||||
id="reset-btn"
|
||||
title="Reset"
|
||||
data-i18n-title="multiTool.reset"
|
||||
style="display: none"
|
||||
class="flex items-center gap-1.5 bg-gray-700 hover:bg-gray-600 text-white px-2.5 sm:px-3 py-1.5 rounded-lg text-sm transition-colors"
|
||||
>
|
||||
@@ -158,6 +160,8 @@
|
||||
|
||||
<button
|
||||
id="faq-open-btn"
|
||||
title="FAQ"
|
||||
data-i18n-title="footer.faqLink"
|
||||
type="button"
|
||||
class="flex items-center gap-1.5 bg-gray-700 hover:bg-gray-600 text-white px-2.5 sm:px-3 py-1.5 rounded-lg text-sm transition-colors"
|
||||
aria-label="FAQ"
|
||||
@@ -171,6 +175,8 @@
|
||||
|
||||
<button
|
||||
id="close-tool-btn"
|
||||
title="Close"
|
||||
data-i18n-title="common.close"
|
||||
class="flex items-center gap-1.5 bg-gray-700 hover:bg-gray-600 text-white px-2.5 sm:px-3 py-1.5 rounded-lg text-sm transition-colors"
|
||||
aria-label="Close"
|
||||
>
|
||||
|
||||
@@ -315,12 +315,14 @@
|
||||
>
|
||||
<button
|
||||
id="toolbox-toggle"
|
||||
title="Toolbox"
|
||||
class="md:hidden bg-gray-700 hover:bg-gray-600 text-white p-1.5 rounded-lg transition-colors"
|
||||
>
|
||||
<i class="ph ph-plus-circle text-lg"></i>
|
||||
</button>
|
||||
<button
|
||||
id="run-btn"
|
||||
title="Run"
|
||||
class="bg-indigo-600 hover:bg-indigo-700 text-white font-semibold px-3 md:px-4 py-1.5 rounded-lg flex items-center gap-1.5 text-sm transition-colors"
|
||||
>
|
||||
<i class="ph ph-play text-base"></i>
|
||||
@@ -328,6 +330,7 @@
|
||||
</button>
|
||||
<button
|
||||
id="clear-btn"
|
||||
title="Clear"
|
||||
class="bg-gray-700 hover:bg-gray-600 text-white px-2.5 md:px-3 py-1.5 rounded-lg text-sm flex items-center gap-1 transition-colors"
|
||||
>
|
||||
<i class="ph ph-trash text-base md:hidden"></i>
|
||||
@@ -336,6 +339,7 @@
|
||||
<div class="flex-1"></div>
|
||||
<button
|
||||
id="save-btn"
|
||||
title="Save"
|
||||
class="bg-gray-700 hover:bg-gray-600 text-white px-2.5 md:px-3 py-1.5 rounded-lg text-sm flex items-center gap-1 transition-colors"
|
||||
>
|
||||
<i class="ph ph-floppy-disk text-sm"></i>
|
||||
@@ -343,6 +347,7 @@
|
||||
</button>
|
||||
<button
|
||||
id="faq-open-btn"
|
||||
title="FAQ"
|
||||
type="button"
|
||||
class="bg-gray-700 hover:bg-gray-600 text-white px-2.5 md:px-3 py-1.5 rounded-lg text-sm flex items-center gap-1 transition-colors"
|
||||
aria-haspopup="dialog"
|
||||
@@ -354,6 +359,7 @@
|
||||
</button>
|
||||
<button
|
||||
id="load-btn"
|
||||
title="Load"
|
||||
class="bg-gray-700 hover:bg-gray-600 text-white px-2.5 md:px-3 py-1.5 rounded-lg text-sm flex items-center gap-1 transition-colors"
|
||||
>
|
||||
<i class="ph ph-folder-open text-sm"></i>
|
||||
@@ -361,6 +367,7 @@
|
||||
</button>
|
||||
<button
|
||||
id="export-btn"
|
||||
title="Export"
|
||||
class="bg-gray-700 hover:bg-gray-600 text-white px-2.5 md:px-3 py-1.5 rounded-lg text-sm flex items-center gap-1 transition-colors"
|
||||
>
|
||||
<i class="ph ph-export text-sm"></i>
|
||||
@@ -368,6 +375,7 @@
|
||||
</button>
|
||||
<button
|
||||
id="import-btn"
|
||||
title="Import"
|
||||
class="bg-gray-700 hover:bg-gray-600 text-white px-2.5 md:px-3 py-1.5 rounded-lg text-sm flex items-center gap-1 transition-colors"
|
||||
>
|
||||
<i class="ph ph-download-simple text-sm"></i>
|
||||
|
||||
@@ -3,6 +3,8 @@ import {
|
||||
changeLanguage,
|
||||
getLanguageFromUrl,
|
||||
LANGUAGE_CHOICE_KEY,
|
||||
rewriteLinks,
|
||||
supportedLanguages,
|
||||
} from '@/js/i18n/i18n';
|
||||
|
||||
describe('getLanguageFromUrl', () => {
|
||||
@@ -75,6 +77,22 @@ describe('getLanguageFromUrl', () => {
|
||||
window.location.pathname = '/zh-TW/merge-pdf';
|
||||
expect(getLanguageFromUrl()).toBe('zh-TW');
|
||||
});
|
||||
|
||||
it('should match every supported language from the URL', () => {
|
||||
supportedLanguages.forEach((lang) => {
|
||||
window.location.pathname = `/${lang}/`;
|
||||
expect(getLanguageFromUrl()).toBe(lang);
|
||||
window.location.pathname = `/${lang}/merge-pdf`;
|
||||
expect(getLanguageFromUrl()).toBe(lang);
|
||||
});
|
||||
});
|
||||
|
||||
it('should match Polish from the URL', () => {
|
||||
window.location.pathname = '/pl/';
|
||||
expect(getLanguageFromUrl()).toBe('pl');
|
||||
window.location.pathname = '/pl/merge-pdf';
|
||||
expect(getLanguageFromUrl()).toBe('pl');
|
||||
});
|
||||
});
|
||||
|
||||
describe('changeLanguage', () => {
|
||||
@@ -143,4 +161,55 @@ describe('changeLanguage', () => {
|
||||
changeLanguage('ja');
|
||||
expect(target).toBe('/pdf/ja/merge-pdf');
|
||||
});
|
||||
|
||||
it('swaps the Polish prefix instead of nesting it', () => {
|
||||
at('/pl/merge-pdf');
|
||||
changeLanguage('de');
|
||||
expect(target).toBe('/de/merge-pdf');
|
||||
at('/pl/');
|
||||
changeLanguage('en');
|
||||
expect(target).toBe('/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('rewriteLinks', () => {
|
||||
const originalLocation = window.location;
|
||||
|
||||
function at(pathname: string) {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { ...originalLocation, pathname },
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
vi.stubEnv('BASE_URL', '/');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: originalLocation,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
document.body.innerHTML = '';
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it('prefixes links on Polish pages and leaves language links alone', () => {
|
||||
at('/pl/merge-pdf');
|
||||
document.body.innerHTML =
|
||||
'<a href="/tools">tools</a>' +
|
||||
'<a href="/de/merge-pdf">de</a>' +
|
||||
'<a href="/pl/merge-pdf">pl</a>';
|
||||
|
||||
rewriteLinks();
|
||||
|
||||
const links = document.querySelectorAll('a');
|
||||
expect(links[0].getAttribute('href')).toBe('/pl/tools');
|
||||
expect(links[1].getAttribute('href')).toBe('/de/merge-pdf');
|
||||
expect(links[2].getAttribute('href')).toBe('/pl/merge-pdf');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
const PAGES_DIR = path.resolve(__dirname, '../pages');
|
||||
|
||||
function loadPage(name: string): Document {
|
||||
const html = fs.readFileSync(path.join(PAGES_DIR, name), 'utf8');
|
||||
return new DOMParser().parseFromString(html, 'text/html');
|
||||
}
|
||||
|
||||
describe('top bar hover text', () => {
|
||||
it.each(['pdf-multi-tool.html', 'pdf-workflow.html'])(
|
||||
'gives every %s button that hides its label a title',
|
||||
(page) => {
|
||||
const labels = loadPage(page).querySelectorAll(
|
||||
'button span[class*="hidden"][class*=":inline"]'
|
||||
);
|
||||
|
||||
expect(labels.length).toBeGreaterThan(0);
|
||||
labels.forEach((label) => {
|
||||
const button = label.closest('button');
|
||||
expect(button?.title, button?.id).toBe(label.textContent?.trim());
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -167,15 +167,43 @@ describe('validatePdfSignatures extraction and coverage', () => {
|
||||
const pdfBytes = new TextEncoder().encode(
|
||||
'%PDF-1.7\n' +
|
||||
'1 0 obj\n<< /Subtype /Text /Contents (A note) >>\nendobj\n' +
|
||||
'2 0 obj\n<< /Filter /Adobe.PPKLite /SubFilter /ETSI.RFC3161 ' +
|
||||
'/ByteRange [0 10 20 10] /Contents <00> >>\nendobj\n' +
|
||||
'3 0 obj\n<< /Type /DocTimeStamp /ByteRange [0 10 20 10] ' +
|
||||
'2 0 obj\n<< /Type /Annot /ByteRange [0 10 20 10] ' +
|
||||
'/Contents <00> >>\nendobj\n%%EOF\n'
|
||||
);
|
||||
|
||||
expect(countSignatures(pdfBytes)).toBe(0);
|
||||
});
|
||||
|
||||
it('reports document timestamps as timestamps, typed or not', () => {
|
||||
const pdfBytes = new TextEncoder().encode(
|
||||
'%PDF-1.7\n' +
|
||||
'1 0 obj\n<< /Filter /Adobe.PPKLite /SubFilter /ETSI.RFC3161 ' +
|
||||
'/ByteRange [0 10 20 10] /Contents <00> >>\nendobj\n' +
|
||||
'2 0 obj\n<< /Type /DocTimeStamp /ByteRange [0 10 20 10] ' +
|
||||
'/Contents <00> >>\nendobj\n%%EOF\n'
|
||||
);
|
||||
|
||||
const signatures = extractSignatures(pdfBytes);
|
||||
expect(signatures).toHaveLength(2);
|
||||
expect(signatures.every((sig) => sig.isTimestamp)).toBe(true);
|
||||
});
|
||||
|
||||
it('ignores Reason, Location, ContactInfo and Name on document timestamps', () => {
|
||||
const pdfBytes = new TextEncoder().encode(
|
||||
'%PDF-1.7\n' +
|
||||
'1 0 obj\n<< /Type /DocTimeStamp /SubFilter /ETSI.RFC3161 ' +
|
||||
'/Reason (r) /Location (l) /ContactInfo (c) /Name (n) ' +
|
||||
'/ByteRange [0 10 20 10] /Contents <00> >>\nendobj\n%%EOF\n'
|
||||
);
|
||||
|
||||
const [signature] = extractSignatures(pdfBytes);
|
||||
expect(signature.isTimestamp).toBe(true);
|
||||
expect(signature.reason).toBeUndefined();
|
||||
expect(signature.location).toBeUndefined();
|
||||
expect(signature.contactInfo).toBeUndefined();
|
||||
expect(signature.name).toBeUndefined();
|
||||
});
|
||||
|
||||
it('reports an intact signature as partial when an incremental update follows', async () => {
|
||||
const signed = await signPdf(samplePdf, certificateData, {
|
||||
signatureInfo: { reason: 'Original' },
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import forge from 'node-forge';
|
||||
import { validatePdfSignatures } from '@/js/logic/validate-signature-pdf';
|
||||
|
||||
const TST_INFO_OID = '1.2.840.113549.1.9.16.1.4';
|
||||
const RANGE_PLACEHOLDER = '{{RANGE}}';
|
||||
const RANGE_WIDTH = 64;
|
||||
const HEX_LENGTH = 8192;
|
||||
const HEAD =
|
||||
'%PDF-1.7\n1 0 obj\n<< /Type /DocTimeStamp /Filter /Adobe.PPKLite ' +
|
||||
`/SubFilter /ETSI.RFC3161 ${RANGE_PLACEHOLDER} /Contents `;
|
||||
const TAIL = ' >>\nendobj\ntrailer\n<< /Root 1 0 R >>\n%%EOF\n';
|
||||
|
||||
const keys = forge.pki.rsa.generateKeyPair(1024);
|
||||
const tsaCert = forge.pki.createCertificate();
|
||||
tsaCert.publicKey = keys.publicKey;
|
||||
tsaCert.serialNumber = '02';
|
||||
tsaCert.validity.notBefore = new Date(Date.now() - 86400000);
|
||||
tsaCert.validity.notAfter = new Date(Date.now() + 86400000 * 365);
|
||||
tsaCert.setSubject([{ name: 'commonName', value: 'Test TSA' }]);
|
||||
tsaCert.setIssuer([{ name: 'commonName', value: 'Test TSA' }]);
|
||||
tsaCert.sign(keys.privateKey, forge.md.sha256.create());
|
||||
|
||||
const { asn1 } = forge;
|
||||
const node = (type: number, value: string | forge.asn1.Asn1[]) =>
|
||||
asn1.create(asn1.Class.UNIVERSAL, type, Array.isArray(value), value);
|
||||
const oid = (id: string) => node(asn1.Type.OID, asn1.oidToDer(id).getBytes());
|
||||
|
||||
function tstInfoDer(imprint: string, imprintOid: string): string {
|
||||
return asn1
|
||||
.toDer(
|
||||
node(asn1.Type.SEQUENCE, [
|
||||
node(asn1.Type.INTEGER, asn1.integerToDer(1).getBytes()),
|
||||
oid('1.2.3.4.1'),
|
||||
node(asn1.Type.SEQUENCE, [
|
||||
node(asn1.Type.SEQUENCE, [oid(imprintOid), node(asn1.Type.NULL, '')]),
|
||||
node(asn1.Type.OCTETSTRING, imprint),
|
||||
]),
|
||||
node(asn1.Type.INTEGER, asn1.integerToDer(42).getBytes()),
|
||||
node(asn1.Type.GENERALIZEDTIME, '20260102030405Z'),
|
||||
])
|
||||
)
|
||||
.getBytes();
|
||||
}
|
||||
|
||||
/** An RFC 3161 token whose TSTInfo carries the given document hash. */
|
||||
function timestampTokenHex(imprint: string, imprintOid: string): string {
|
||||
const p7 = forge.pkcs7.createSignedData();
|
||||
p7.content = forge.util.createBuffer(tstInfoDer(imprint, imprintOid));
|
||||
p7.addCertificate(tsaCert);
|
||||
p7.addSigner({
|
||||
key: keys.privateKey,
|
||||
certificate: tsaCert,
|
||||
digestAlgorithm: forge.pki.oids.sha256,
|
||||
authenticatedAttributes: [
|
||||
{ type: forge.pki.oids.contentType, value: TST_INFO_OID },
|
||||
{ type: forge.pki.oids.messageDigest },
|
||||
],
|
||||
});
|
||||
p7.sign();
|
||||
const contentInfo = p7.toAsn1();
|
||||
// node-forge always labels the content as id-data; a TSA labels it TSTInfo
|
||||
const signedData = (contentInfo.value as forge.asn1.Asn1[])[1]
|
||||
.value as forge.asn1.Asn1[];
|
||||
const encap = (signedData[0].value as forge.asn1.Asn1[])[2]
|
||||
.value as forge.asn1.Asn1[];
|
||||
encap[0] = oid(TST_INFO_OID);
|
||||
return forge.util.bytesToHex(asn1.toDer(contentInfo).getBytes());
|
||||
}
|
||||
|
||||
function sha256(content: string): string {
|
||||
const md = forge.md.sha256.create();
|
||||
md.update(content);
|
||||
return md.digest().bytes();
|
||||
}
|
||||
|
||||
function buildTimestampedPdf(
|
||||
imprintFor: (signedContent: string) => string = sha256,
|
||||
imprintOid: string = forge.pki.oids.sha256
|
||||
): Uint8Array {
|
||||
const headLength = HEAD.length - RANGE_PLACEHOLDER.length + RANGE_WIDTH;
|
||||
const gapLength = HEX_LENGTH + 2;
|
||||
const byteRange = [0, headLength, headLength + gapLength, TAIL.length];
|
||||
const head = HEAD.replace(
|
||||
RANGE_PLACEHOLDER,
|
||||
`/ByteRange [${byteRange.join(' ')}]`.padEnd(RANGE_WIDTH)
|
||||
);
|
||||
const hex = timestampTokenHex(imprintFor(head + TAIL), imprintOid).padEnd(
|
||||
HEX_LENGTH,
|
||||
'0'
|
||||
);
|
||||
return new TextEncoder().encode(`${head}<${hex}>${TAIL}`);
|
||||
}
|
||||
|
||||
describe('document timestamps (RFC 3161)', () => {
|
||||
it('verifies an intact document timestamp', async () => {
|
||||
const [result] = await validatePdfSignatures(buildTimestampedPdf());
|
||||
|
||||
expect(result.isTimestamp).toBe(true);
|
||||
expect(result.cryptoVerificationStatus).toBe('verified');
|
||||
expect(result.coverageStatus).toBe('full');
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.usesInsecureDigest).toBeFalsy();
|
||||
expect(result.algorithms.imprint).toBe('SHA-256');
|
||||
expect(result.signerName).toBe('Test TSA');
|
||||
expect(result.signatureDate?.toISOString()).toBe(
|
||||
'2026-01-02T03:04:05.000Z'
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['SHA-1', forge.md.sha1, forge.pki.oids.sha1],
|
||||
['MD5', forge.md.md5, forge.pki.oids.md5],
|
||||
])('treats a %s messageImprint as insecure', async (name, algorithm, id) => {
|
||||
const imprintFor = (content: string) => {
|
||||
const md = algorithm.create();
|
||||
md.update(content);
|
||||
return md.digest().bytes();
|
||||
};
|
||||
|
||||
const [result] = await validatePdfSignatures(
|
||||
buildTimestampedPdf(imprintFor, id)
|
||||
);
|
||||
|
||||
expect(result.cryptoVerificationStatus).toBe('verified');
|
||||
expect(result.usesInsecureDigest).toBe(true);
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.algorithms.imprint).toBe(name);
|
||||
});
|
||||
|
||||
it('fails when the document no longer matches the timestamped hash', async () => {
|
||||
const pdfBytes = buildTimestampedPdf();
|
||||
// Rename /Adobe.PPKLite to /Adobe.QPKLite: still a valid PDF, new bytes
|
||||
const at = new TextDecoder('latin1').decode(pdfBytes).indexOf('PPKLite');
|
||||
pdfBytes[at] = 'Q'.charCodeAt(0);
|
||||
|
||||
const [result] = await validatePdfSignatures(pdfBytes);
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.cryptoVerificationStatus).toBe('failed');
|
||||
expect(result.errorMessage).toMatch(/modified after timestamping/);
|
||||
});
|
||||
|
||||
it('fails when the token holds the hash of a different document', async () => {
|
||||
const [result] = await validatePdfSignatures(
|
||||
buildTimestampedPdf(() => sha256('some other document'))
|
||||
);
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.cryptoVerificationStatus).toBe('failed');
|
||||
});
|
||||
|
||||
it('fails when the TSTInfo was altered after the TSA signed it', async () => {
|
||||
const pdfBytes = buildTimestampedPdf();
|
||||
const text = new TextDecoder('latin1').decode(pdfBytes);
|
||||
// GeneralizedTime 20260102030405Z, hex-encoded inside /Contents
|
||||
const timeHex = forge.util.bytesToHex('20260102030405Z');
|
||||
const at = text.indexOf(timeHex);
|
||||
expect(at).toBeGreaterThan(0);
|
||||
pdfBytes[at + 1] = pdfBytes[at + 1] === 0x31 ? 0x32 : 0x31;
|
||||
|
||||
const [result] = await validatePdfSignatures(pdfBytes);
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.cryptoVerificationStatus).toBe('failed');
|
||||
});
|
||||
});
|
||||
@@ -2155,6 +2155,12 @@
|
||||
],
|
||||
"fileCount": 1
|
||||
},
|
||||
"Could not read the timestamp token": {
|
||||
"files": [
|
||||
"logic/validate-signature-pdf.ts"
|
||||
],
|
||||
"fileCount": 1
|
||||
},
|
||||
"Could not reduce file size further. Original: {{originalSize}}, New: {{compressedSize}}.": {
|
||||
"files": [
|
||||
"logic/compress-pdf-page.ts"
|
||||
@@ -2580,12 +2586,24 @@
|
||||
],
|
||||
"fileCount": 1
|
||||
},
|
||||
"Document Timestamp {{n}}": {
|
||||
"files": [
|
||||
"logic/validate-signature-pdf-page.ts"
|
||||
],
|
||||
"fileCount": 1
|
||||
},
|
||||
"Document Type": {
|
||||
"files": [
|
||||
"logic/page-dimensions-page.ts"
|
||||
],
|
||||
"fileCount": 1
|
||||
},
|
||||
"Document hash does not match the timestamp: PDF was modified after timestamping": {
|
||||
"files": [
|
||||
"logic/validate-signature-pdf.ts"
|
||||
],
|
||||
"fileCount": 1
|
||||
},
|
||||
"Document properties": {
|
||||
"files": [
|
||||
"logic/edit-pdf-text-dock.ts"
|
||||
@@ -5222,6 +5240,12 @@
|
||||
],
|
||||
"fileCount": 1
|
||||
},
|
||||
"Modified After Timestamping (timestamped part intact)": {
|
||||
"files": [
|
||||
"logic/validate-signature-pdf-page.ts"
|
||||
],
|
||||
"fileCount": 1
|
||||
},
|
||||
"More": {
|
||||
"files": [
|
||||
"logic/edit-pdf-text-dock.ts"
|
||||
@@ -8903,6 +8927,30 @@
|
||||
],
|
||||
"fileCount": 1
|
||||
},
|
||||
"Timestamp Imprint Algorithm:": {
|
||||
"files": [
|
||||
"logic/validate-signature-pdf-page.ts"
|
||||
],
|
||||
"fileCount": 1
|
||||
},
|
||||
"Timestamp Intact: Timestamp Authority Not Verified": {
|
||||
"files": [
|
||||
"logic/validate-signature-pdf-page.ts"
|
||||
],
|
||||
"fileCount": 1
|
||||
},
|
||||
"Timestamped By": {
|
||||
"files": [
|
||||
"logic/validate-signature-pdf-page.ts"
|
||||
],
|
||||
"fileCount": 1
|
||||
},
|
||||
"Timestamped On": {
|
||||
"files": [
|
||||
"logic/validate-signature-pdf-page.ts"
|
||||
],
|
||||
"fileCount": 1
|
||||
},
|
||||
"Tint": {
|
||||
"files": [
|
||||
"workflow/nodes/adjust-colors-node.ts"
|
||||
@@ -9440,6 +9488,12 @@
|
||||
],
|
||||
"fileCount": 1
|
||||
},
|
||||
"Valid Timestamp": {
|
||||
"files": [
|
||||
"logic/validate-signature-pdf-page.ts"
|
||||
],
|
||||
"fileCount": 1
|
||||
},
|
||||
"Valid Until": {
|
||||
"files": [
|
||||
"logic/validate-signature-pdf-page.ts"
|
||||
|
||||
Reference in new issue
Block a user