Merge pull request #8 from kuba2k2/master
[hebe] [insomnia-plugin] Add new implementation
This commit is contained in:
commit
502e59f1e5
5 changed files with 169 additions and 0 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -62,5 +62,6 @@ typings/
|
|||
.idea/
|
||||
|
||||
vendor
|
||||
package-lock.json
|
||||
*.iml
|
||||
android/lib/src/main/gen
|
||||
|
|
20
hebe-insomnia-plugin/README.md
Normal file
20
hebe-insomnia-plugin/README.md
Normal 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.
|
31
hebe-insomnia-plugin/package.json
Normal file
31
hebe-insomnia-plugin/package.json
Normal 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"
|
||||
}
|
||||
}
|
67
hebe-insomnia-plugin/plugin.js
Normal file
67
hebe-insomnia-plugin/plugin.js
Normal 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()
|
||||
});
|
||||
}
|
50
hebe-insomnia-plugin/signer.js
Normal file
50
hebe-insomnia-plugin/signer.js
Normal 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;
|
Loading…
Reference in a new issue