diff --git a/package-lock.json b/package-lock.json index d286339..947e608 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "nodemailer": "^6.7.2", "pg": "^8.5.1", "pg-hstore": "^2.3.3", + "pkijs": "^3.0.16", "rate-limiter-flexible": "^2.1.15", "sequelize": "^6.25.5", "socket.io": "^4.0.1", @@ -2540,6 +2541,24 @@ "node": ">=8" } }, + "node_modules/asn1js": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.5.tgz", + "integrity": "sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==", + "dependencies": { + "pvtsutils": "^1.3.2", + "pvutils": "^1.1.3", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/asn1js/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, "node_modules/async": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", @@ -2764,6 +2783,14 @@ "node": ">= 0.8" } }, + "node_modules/bytestreamjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", + "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/cacache": { "version": "15.3.0", "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", @@ -5915,6 +5942,26 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkijs": { + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.0.16.tgz", + "integrity": "sha512-iDUm90wfgtfd1PDV1oEnQj/4jBIU9hCSJeV0kQKThwDpbseFxC4TdpoMYlwE9maol5u0wMGZX9cNG2h1/0Lhww==", + "dependencies": { + "asn1js": "^3.0.5", + "bytestreamjs": "^2.0.0", + "pvtsutils": "^1.3.2", + "pvutils": "^1.1.3", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/pkijs/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, "node_modules/please-upgrade-node": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", @@ -6016,6 +6063,27 @@ "node": ">=6" } }, + "node_modules/pvtsutils": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.5.tgz", + "integrity": "sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==", + "dependencies": { + "tslib": "^2.6.1" + } + }, + "node_modules/pvtsutils/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/pvutils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz", + "integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -9712,6 +9780,23 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, + "asn1js": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.5.tgz", + "integrity": "sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==", + "requires": { + "pvtsutils": "^1.3.2", + "pvutils": "^1.1.3", + "tslib": "^2.4.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, "async": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", @@ -9868,6 +9953,11 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" }, + "bytestreamjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", + "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==" + }, "cacache": { "version": "15.3.0", "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", @@ -12148,6 +12238,25 @@ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true }, + "pkijs": { + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.0.16.tgz", + "integrity": "sha512-iDUm90wfgtfd1PDV1oEnQj/4jBIU9hCSJeV0kQKThwDpbseFxC4TdpoMYlwE9maol5u0wMGZX9cNG2h1/0Lhww==", + "requires": { + "asn1js": "^3.0.5", + "bytestreamjs": "^2.0.0", + "pvtsutils": "^1.3.2", + "pvutils": "^1.1.3", + "tslib": "^2.4.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, "please-upgrade-node": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", @@ -12225,6 +12334,26 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, + "pvtsutils": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.5.tgz", + "integrity": "sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==", + "requires": { + "tslib": "^2.6.1" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "pvutils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz", + "integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==" + }, "qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", diff --git a/package.json b/package.json index b62ac2f..150a1b0 100644 --- a/package.json +++ b/package.json @@ -60,10 +60,10 @@ "nodemailer": "^6.7.2", "pg": "^8.5.1", "pg-hstore": "^2.3.3", + "pkijs": "^3.0.16", "rate-limiter-flexible": "^2.1.15", "sequelize": "^6.25.5", "socket.io": "^4.0.1", - "sqlite3": "^4.0.0", "umzug": "^2.3.0" }, "optionalDependencies": { diff --git a/src/api/auth.ts b/src/api/auth.ts index 895f58e..167d187 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -26,13 +26,15 @@ import { isSendMailLoginCodeRequest, isSignInByMailCodeRequest } from './validator' +import { analyze } from './integrity' export const createAuthRouter = (database: Database) => { const router = Router() router.post('/send-mail-login-code-v2', json(), async (req, res, next) => { const info = { - ua: req.headers['user-agent'] + ua: req.headers['user-agent'], + cert: analyze(req), } try { diff --git a/src/api/integrity.ts b/src/api/integrity.ts new file mode 100644 index 0000000..4e1975a --- /dev/null +++ b/src/api/integrity.ts @@ -0,0 +1,104 @@ +import { X509Certificate } from 'crypto' +import { Request } from 'express' +import { fromBER, Sequence, Integer, OctetString, Set } from 'asn1js' +import { Certificate } from 'pkijs' + +export interface CertInfo { + raw: string + applicationCerts: Array +} + +export function analyze(req: Request): CertInfo | null { + try { + const certStr = req.headers['clientcertificate'] + + if (typeof certStr !== 'string') return null + + const nativeCert = new X509Certificate(decodeURIComponent(certStr)) + + const now = Date.now() + const from = Date.parse(nativeCert.validFrom) + const to = Date.parse(nativeCert.validTo) + + if (from > now || to < now) return null + if (from > to || from + 1000 * 60 * 5 < to) return null + + const cert1 = fromBER(nativeCert.raw) + + if (cert1.offset === -1) return null + + const cert2 = new Certificate({ schema: cert1.result }) + const androidExtension = (cert2.extensions || []).find((item) => item.extnID === '1.3.6.1.4.1.11129.2.1.17')?.extnValue?.valueBlock?.valueHexView + + if (!androidExtension) return null + + const androidExtensionParsed = fromBER(androidExtension) + + if (androidExtensionParsed.offset === -1) return null + + const androidExtensionSequence = androidExtensionParsed.result + + if (!(androidExtensionSequence instanceof Sequence)) return null + + const versionInteger = androidExtensionSequence.valueBlock.value[0] + + if (!(versionInteger instanceof Integer)) return null + + const versionValue = versionInteger.valueBlock.valueDec + + const authorizationLists = androidExtensionSequence.valueBlock.value.slice(6, 8) + + let applicationId = null + + for (const authList of authorizationLists) { + if (!(authList instanceof Sequence)) continue + + for (const authListItem of authList.valueBlock.value) { + if ( + // version 1 does not provide this data structure + versionValue !== 1 && + authListItem.idBlock.tagNumber === 709 + ) { + if (!('value' in authListItem.valueBlock)) continue + + const value = (authListItem.valueBlock as unknown as { value: object }).value + + if (!Array.isArray(value) || value.length !== 1) continue + + if (value[0] instanceof OctetString) applicationId = value[0] + } + } + } + + if (!applicationId) return null + + const parsedApplicationId = fromBER(applicationId.valueBlock.valueHexView) + + if (parsedApplicationId.offset === -1) return null + + if (!(parsedApplicationId.result instanceof Sequence)) return null + + const parsedApplicationIdInfo = parsedApplicationId.result.valueBlock.value + + if (parsedApplicationIdInfo.length !== 2) return null + + const signatureDigests = parsedApplicationIdInfo[1] + + if (!(signatureDigests instanceof Set)) return null + + const applicationCerts = [] + + for (const cert of signatureDigests.valueBlock.value) { + if (cert instanceof OctetString) { + applicationCerts.push(Buffer.from(cert.valueBlock.valueHexView).toString('hex')) + } + } + + return { + raw: nativeCert.raw.toString('base64'), + applicationCerts + } + } catch (ex) { + return null + } +}