Merge pull request #8 from kuba2k2/master

[hebe] [insomnia-plugin] Add new implementation
This commit is contained in:
Mikołaj Pich 2020-03-14 13:25:07 +01:00 committed by GitHub
commit 502e59f1e5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 169 additions and 0 deletions

1
.gitignore vendored
View file

@ -62,5 +62,6 @@ typings/
.idea/
vendor
package-lock.json
*.iml
android/lib/src/main/gen

View file

@ -0,0 +1,20 @@
# Insomnia Dzienniczek+ 2020 request signer
[![npm](https://img.shields.io/npm/v/@wulkanowy/insomnia-plugin-hebe-request-signer.svg?style=flat-square)](https://www.npmjs.com/package/@wulkanowy/insomnia-plugin-hebe-request-signer)
This is a plugin for [Insomnia](https://insomnia.rest) that allows users to automatically sign requests to the API of Dzienniczek+ 2020 (codenamed `hebe`) app.
## Installation
Install the `@wulkanowy/insomnia-plugin-hebe-request-signer` plugin from Preferences > Plugins.
## Usage
Add to `Headers`:
- `PrivateKey` - API certificate's private key in PEM format - plain Base64 (no headers)
- `Fingerprint` - API certificate's SHA1 sum/fingerprint in hex format (no delimiters, lowercase)
- `DeviceModel` - user's Android device model, used to provide `vDeviceModel` request header
- `FirebaseToken` - user's Android device Firebase token - used to send push notifications to device. May be fake, not recommended.
A plugin automatically populating headers (such as [insomnia-plugin-headers](https://www.npmjs.com/package/insomnia-plugin-headers))
may be installed to simplify API usage.

View file

@ -0,0 +1,31 @@
{
"name": "insomnia-plugin-hebe-request-signer",
"version": "0.1.0",
"description": "Dzienniczek+ 2020 (hebe) Request Signer by Oranż Metylowy",
"main": "plugin.js",
"insomnia": {
"name": "hebe-request-signer",
"description": "Dzienniczek+ 2020 (hebe) Request Signer by Oranż Metylowy"
},
"repository": {
"type": "git",
"url": "git+https://github.com/wulkanowy/uonet-request-signer.git"
},
"keywords": [
"uonet",
"wulkanowy",
"insomnia-plugin",
"hebe",
"dzienniczek",
"vulcan"
],
"author": "Kuba Szczodrzyński <kuba@szczodrzynski.pl>",
"bugs": {
"url": "https://github.com/wulkanowy/uonet-request-signer/issues"
},
"homepage": "https://github.com/wulkanowy/uonet-request-signer/tree/master/insomnia-plugin#readme",
"license": "MIT",
"dependencies": {
"uuid": "^3.3.3"
}
}

View file

@ -0,0 +1,67 @@
const uuid = require('uuid/v4');
const {getSignatureValues} = require("./signer");
function getOrThrow(context, name) {
let header = context.request.getHeader(name);
if (header === null || header.length === 0) {
throw `No ${name} header`;
}
return header;
}
module.exports.requestHooks = [
async (context) => {
if (!context.request.hasHeader('PrivateKey')) return;
const pkey = getOrThrow(context, "PrivateKey");
const fingerprint = getOrThrow(context, "Fingerprint");
const deviceModel = getOrThrow(context, "DeviceModel");
const firebaseToken = getOrThrow(context, "FirebaseToken");
let timestamp = +context.request.getHeader('Timestamp');
if (timestamp === 0) timestamp = +new Date();
const timestampStrHeader = new Date(timestamp + 1000).toUTCString();
const body = getWrappedBody(context.request.getMethod(), context.request.getBodyText(), fingerprint, firebaseToken, timestamp);
const {signature, digest, canonicalUrl} = getSignatureValues(fingerprint, pkey, body, context.request.getUrl(), timestamp);
// set body
context.request.setBodyText(body);
// set headers
context.request.setHeader('User-Agent', 'okhttp/3.11.0');
context.request.setHeader('vOS', 'Android');
context.request.setHeader('vDeviceModel', deviceModel);
context.request.setHeader('vAPI', 1);
context.request.setHeader('vDate', timestampStrHeader);
context.request.setHeader('vCanonicalUrl', canonicalUrl);
context.request.setHeader('Signature', signature);
if (body != null) context.request.setHeader('Digest', digest);
// cleaning...
context.request.removeHeader('PrivateKey');
context.request.removeHeader('Fingerprint');
context.request.removeHeader('DeviceModel');
context.request.removeHeader('Timestamp');
context.request.removeHeader('FirebaseToken');
}
];
function getWrappedBody(method, body, fingerprint, firebaseToken, timestamp) {
if (method !== 'POST') {
if (method !== 'GET') throw 'Incorrect request method (GET or POST).';
else return null;
}
return JSON.stringify({
AppName: 'DzienniczekPlus 2.0',
AppVersion: '1.0',
CertificateId: fingerprint,
Envelope: JSON.parse(body),
FirebaseToken: firebaseToken,
API: 1,
RequestId: uuid(),
Timestamp: timestamp,
TimestampFormatted: new Date(timestamp + 3600 * 1000 + 1000).toUTCString()
});
}

View file

@ -0,0 +1,50 @@
const aCrypto = require('crypto');
function getDigest(body) {
if (body == null) return "";
const hash = aCrypto.createHash('SHA256');
hash.update(body);
return hash.digest("base64");
}
function getSignatureValue(values, pkey) {
const sign = aCrypto.createSign('RSA-SHA256');
sign.update(values);
return sign.sign("-----BEGIN PRIVATE KEY-----\n" + pkey + "\n-----END PRIVATE KEY-----", "base64");
}
function getCanonicalUrl(fullUrl) {
const url = fullUrl.match("(api/mobile/.+)");
if (url == null) throw 'The URL does not seem correct (does not match `(api/mobile/.+)` regex)';
return encodeURIComponent(url[0]).toLowerCase();
}
function getHeadersList(body, digest, canonicalUrl, timestamp) {
const signData = [
['vCanonicalUrl', canonicalUrl],
body == null ? null : ['Digest', digest],
['vDate', new Date(timestamp + 1000).toUTCString()]
].filter(item => !!item);
return {
"headers": signData.map(item => item[0]).join(" "),
"values": signData.map(item => item[1]).join("")
};
}
function getSignatureValues(fingerprint, pkey, body, fullUrl, timestamp) {
const canonicalUrl = getCanonicalUrl(fullUrl);
const digest = getDigest(body);
const {headers, values} = getHeadersList(body, digest, canonicalUrl, timestamp);
const signatureValue = getSignatureValue(values, pkey);
return {
"digest": `SHA-256=${digest}`,
"canonicalUrl": canonicalUrl,
"signature": `keyId="${fingerprint}",headers="${headers}",algorithm="sha256withrsa",signature=Base64(SHA256withRSA(${signatureValue}))`
};
}
exports.getSignatureValues = getSignatureValues;