Skip to content

Commit

Permalink
v1.0.11 → v1.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
YuigaWada committed May 17, 2020
1 parent abff4b5 commit 0ebf818
Show file tree
Hide file tree
Showing 67 changed files with 3,548 additions and 1,236 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ jobs:
- uses: actions/checkout@v2
- name: Select Xcode version
run: sudo xcode-select -s '/Applications/Xcode_11.3.1.app/Contents/Developer'
- name: Inject api url
run: echo ${{ secrets.API_KEY_MANAGER_CODE }} > ./MissCat/Others/App/ApiKeyManager.swift
- name: Cache CocoaPods files
uses: actions/cache@v1
with:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,4 @@ Pods


MissCat.xcworkspace/xcuserdata/yuigawada.xcuserdatad/xcdebugger/Expressions.xcexplist
MissCat/Others/App/ApiKeyManager.swift
14 changes: 14 additions & 0 deletions ApiServer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# webPushDecipher.js
[```'sw/register'```](https://misskey.io/api-doc#operation/sw/register)で購読したPush通知をdecryptするヤツ

### **参考にしたもの**

- https://tools.ietf.org/html/rfc8188

- https://tools.ietf.org/html/rfc8291

- https://tools.ietf.org/html/rfc8291#appendix-A

- https://gist.github.com/tateisu/685eab242549d9c9ffc85020f09a4b71

- https://mastodon.juggler.jp/@tateisu/104098620591598243
73 changes: 73 additions & 0 deletions ApiServer/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
'use strict';

const fs = require('fs');
const express = require('express');
const bodyParser = require('body-parser');

const notification = require('./notification.js');
const webPushDecipher = require('./webPushDecipher.js');

// For Distributed
const authSecret = "Q8Zgu-WDvN5EDT_emFGovQ"
const publicKey = "BJNAJpIOIJnXVVgCTAd4geduXEsNKre0XVvz0j-E_z-8CbGI6VaRPsVI7r-hF88MijMBZApurU2HmSNQ4e-cTmA"
const privateKey = fs.readFileSync('./key/edch_private_key.txt', 'utf8');


// For test
// const authSecret = "43w_wOVYeF9XzyRyZL3O8g"
// const publicKey = "BJgVD2cj1pNKNR2Ss3U_8e7P9AyoL5kWaxVio5aO16Cvnx-P1r7HH8SRb-h5tuxaydZ1ky3oO0V40s6t_uN1SdA"
// const privateKey = "ciQ800G-6jyKWf6KKG94g5rCSU_l_rgbHbyHny_UsIM"



function decrypt(raw) {
// const converted = raw.toString('utf-8') // for debug
const converted = raw.toString('base64')

const reciverKey = webPushDecipher.reciverKeyBuilder(publicKey,privateKey,authSecret)
var decrypted = webPushDecipher.decrypt(converted,reciverKey,false)
return decrypted
}

const app = express();

var concat = require('concat-stream');
app.use(function(req, res, next){
req.pipe(concat(function(data){
req.body = data;
next();
}));
});


app.post("/api/:version/push/:lang/:userId/:deviceToken", function(req, res){
if (req.params.version != "v1") { res.status(410).send('Invalid Version.').end(); }

const rawBody = req.body;
if (!rawBody) { res.status(200).send('Invalid Body.').end(); }

const rawJson = decrypt(rawBody);
const userId = req.params.userId;
const deviceToken = req.params.deviceToken;
const lang = req.params.lang;
if (!rawJson||!userId||!deviceToken||!lang) { res.status(410).send('Invalid Url.').end(); }

console.log(rawJson)
const contents = notification.generateContents(rawJson,lang);
const title = contents[0];
const body = contents[1];
if (!title) { res.status(200).send('Invalid Json.').end(); }

// console.log("deviceToken",deviceToken);
notification.send(deviceToken, title, body); // send!
res.status(200).send('Ok').end();
});

// Start the server
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
console.log(`App listening on port ${PORT}`);
console.log('Press Ctrl+C to quit.');
});

module.exports = app;
Empty file.
Empty file.
10 changes: 10 additions & 0 deletions ApiServer/key/keyGenerator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const crypto = require('crypto');
const util = require('util');
const urlsafeBase64 = require('urlsafe-base64');

const keyCurve = crypto.createECDH('prime256v1');
keyCurve.generateKeys();

console.log("public:", urlsafeBase64.encode(keyCurve.getPublicKey()));
console.log("private:", urlsafeBase64.encode(keyCurve.getPrivateKey()));
console.log("auth:", urlsafeBase64.encode(crypto.randomBytes(16)));
64 changes: 64 additions & 0 deletions ApiServer/notification.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
const fcmNode = require('fcm-node');
const serverKey = require('./key/fcm_private_key.json');
const fcm = new fcmNode(serverKey);

exports.generateContents = function(rawJson, lang) {
const json = JSON.parse(rawJson);
const body = json.body;
if (json.type != "notification") { return null; }

const type = body.type;
const fromUser = body.user.name != null ? body.user.name : body.user.username;

// cf. https://github.com/YuigaWada/MissCat/blob/develop/MissCat/Model/Main/NotificationModel.swift
if (type == "reaction") {
const reaction = body.reaction;
const myNote = body.note.text;

var title = fromUser + "さんがリアクション\"" + reaction+ "\"を送信しました";
var message = myNote;
return [title,message];
}
else if (type == "follow") {
const hostLabel = body.user.host != null ? "@" + body.user.host : ""; // 自インスタンスの場合 host == nullになる
var title = "";
var message = "@" + body.user.username + hostLabel + "さんに" + "フォローされました";
return [title,message];
}
else if (type == "reply") {
var title = fromUser + "さんの返信:";
var message = body.note.text;
return [title,message];
}
else if (type == "renote" || type == "quote") {
const justRenote = body.note.text == null; // 引用RNでなければ body.note.text == null
var renoteKind = justRenote ? "" : "引用";

var title = fromUser + "さんが" + renoteKind + "Renoteしました";
var message = justRenote ? body.note.renote.text : body.note.text;
return [title,message];
}

return [null,null]
}


exports.send = function (token, title, body) {
var message = {
to: token,
// collapse_key: key,

notification: {
title: title,
body: body,
badge: "1"
}
};

fcm.send(message, function(error, response){
const success = error == null
if (!success) {
console.log("FCM Error:",error);
}
});
}
Empty file added ApiServer/test/fcm_token.txt
Empty file.
1 change: 1 addition & 0 deletions ApiServer/test/mock.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"type":"notification","body":{"id":"86x615sq97","createdAt":"2020-05-05T07:38:42.458Z","type":"reaction","userId":"84micuph6b","user":{"id":"84micuph6b","name":null,"username":"Wt","host":"misskey.dev","avatarUrl":"https://misskey.io/avatar/84micuph6b","avatarColor":null,"emojis":[{"name":"misscat","host":"misskey.dev","url":"https://s3.arkjp.net/dev/f311ae70-8f91-4154-be51-4199815bae94.png","aliases":[]}]},"note":{"id":"86wduzuy8w","createdAt":"2020-05-04T18:30:05.578Z","userId":"7ze0f2goa7","user":{"id":"7ze0f2goa7","name":"だわくん:miyano_yay:","username":"wada","host":null,"avatarUrl":"https://s3.arkjp.net/misskey/thumbnail-f340c8e4-8951-495d-b7d9-12fd3151f9b2.jpg","avatarColor":"rgb(165,151,172)","isCat":true,"emojis":[{"name":"miyano_yay","host":null,"url":"https://emoji.arkjp.net/misskey/miyano_yay.png","aliases":["miyano","yay"]}]},"text":"やっと仕様を理解したので明日実験してみよう...","cw":null,"visibility":"public","renoteCount":0,"repliesCount":0,"reactions":{":[email protected]:":1},"emojis":[{"name":"[email protected]","url":"https://s3.arkjp.net/dev/f311ae70-8f91-4154-be51-4199815bae94.png"}],"fileIds":[],"files":[],"replyId":null,"renoteId":null},"reaction":":[email protected]:"}}
1 change: 1 addition & 0 deletions ApiServer/test/mock.text
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
B8TZpK9vmRlCbkFTLG6l5gAAEABBBE8bArSb8EH1d8PH0J4Wrf/p2CY2rLUx55TgRayuyH0B3ZjJ2JiMDJH+c2FsA526yd08GVf7QjwqGWnNo4+LwpI4dyaI36CvBMPCf1lOAF50FV7JkgvMGyuYnzgUOh5KSvjDygDpygjRdFYI7amKXXCeRSMcwqn8lXgP1G6CUI43z88+bg/piRVBEnnALn2d60vzUzQNSXUdHAGCQ6aElewpxe3xT74ua0Bxcd4tB3fFaUzpOzYApQRpIlG5ITDTd1haMCuarE5vUGO+oPMsIv1nJO5keRhcvKBWSynj3d0+pGkgajrNjdQentooEQt5GmntKus+mUzLT+UQN1KNWnR3FN9LjsXX8fTk3Vhl7NabiW+N/vDPxI0/lw0VdBNeI460XcWi8aJd6Yb4THB0DjJ5p2JwXHB3zZ1dGSOA6f2hQWTQbVM/9Kisji0SEYdmysFNJaiajax2IeP8eG5lmJ/Lsq6Fs+CCRHnEUZIENEo0Mw3H44Wx/zWG1mEJxffIeuWUFbCcyfD8JYZkvilUu6azhPkTbhidbZ/NQO8oJHU21K5qQbOxiFhPD//cUORE/aCeF8uMdS1PgCvD1I0wfGOxmj3tLOXyvjmTNVzR7jZ7jRMro69/9K2dZ0YrcVMHgZuXfr9OtBD25FoHkkZ6uOxU8rX+FBDt4Af5A3mseX72dUFuhSCYh2akdrhUldvqnjb7IQKQpLrBDh4t4YGmPgXvVs/uSa48MOWNfgWUgng2BReDD7RwT1MeF3KQObAdOaocRZFRWx51JgDv16o7qr5Q/vd5TPIXGKloMgMMwCA5sAsIhmBdBZLjpPrFEspPVJvfGgcu7Hdb/9/xRAOsXBjlcg4iDetdTjkBpr5Gysn1EJfEZZAJaOTD4kgKtKURmynxvtX/nDKLmaIUt1f8Dfo/UKhq+9HcSfoohH/9AcFQCiSPPJrfJFt1BstJi/PhwbmWAMvxwbrzRZXzmXk8iiTEqbnlOqml+dnDZ4aJUjTrCb5OxbfaGsr3kXATcKT8jNKTOK6/0r25NNyGHCXlaF7awdjzQXLamWrsa/ieFmRnrpWKGobW45uNXEwiKmXvDWcVBH5PDeKQpu3IQSdzBXJSpYtrwpwnyyMbI0h+Y8JufjLWFZqNqCpcHG7CiwnKAR3icC3MLbZOS8oxo2AHYfhWXxzuqU5apbuCAzKNtk4N86wEuFLb1XkpI2AJ9u9k8MDBUdRxhxWhgKbQ2EOh4KZeKQDD6olVWR7ECU0otyM1g3+Ej89WOSrqJGhk+S704v1A73+EED0MFaY+MjrIOhCsrgL/Tcu0bbHYjOiuoEFN3Uf75Nr/+qTS6SatkHtqyIi24Y/NWCIZ976946gtym8iWi9aJieCLNYE4IuKjhgIOVrWqo57nbg0/4OQvTubSzBbis3x1X+PrL3IrSKufEs5pzEPuRbyhQfLHJ2gL4v2S0od5v9mxT3XacJAq2rHEd3/GXIxfgwWm/s/aSAIOnxU5DIM71UbVuEKBFtpk0IGt6DzsZiC7lELGUfR/VSv7Hg0d4aMmAsHPmo5r9FDDsALxp2OfL1XMg8/rPnUvRoKjdkY61MHfH7rQoXfy3Yidtx7dHeZ/O21tT/V83HXC6xNXpcGew0wyTjWGBqhVXkRZUjN3EteFniv90gg0SIPW4zzKJ42/uf0tzg76gZiJboAqWwlXmt+FmjnMg4990vA1obltewm6NIRha6EJb4fpafsnA8Hqk/rM7zu57vBLCr22o0VC7gery4xbRMW/es=
15 changes: 15 additions & 0 deletions ApiServer/test/test_decipher.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 29 additions & 0 deletions ApiServer/test/test_join.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@

/** Test: Decrypt → convert json → generate contents → send the notification **/

const webPushDecipher = require('../webPushDecipher.js');
const fs = require('fs');
const notification = require('../notification.js');

publicKey = "BJgVD2cj1pNKNR2Ss3U_8e7P9AyoL5kWaxVio5aO16Cvnx-P1r7HH8SRb-h5tuxaydZ1ky3oO0V40s6t_uN1SdA"
privateKey = "ciQ800G-6jyKWf6KKG94g5rCSU_l_rgbHbyHny_UsIM"
authSecret = "43w_wOVYeF9XzyRyZL3O8g"

function decrypt(raw) {
const reciverKey = webPushDecipher.reciverKeyBuilder(publicKey,privateKey,authSecret);
var decrypted = webPushDecipher.decrypt(raw,reciverKey,false);
return decrypted;
}


const rawBody = fs.readFileSync('./mock.text', 'utf8');
const lang = "ja";

const rawJson = decrypt(rawBody);
const contents = notification.generateContents(rawJson,lang);

console.log("title:",contents[0])
console.log("body:",contents[1])

const token = fs.readFileSync('./fcm_token.txt', 'utf8');
notification.send(token, contents[0], contents[1])
15 changes: 15 additions & 0 deletions ApiServer/test/test_notification.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@

/** Test: generate contents → send the notification **/

const fs = require('fs');
const notification = require('../notification.js');

const rawJson = fs.readFileSync('./mock.json', 'utf8');
const lang = "ja";

const contents = notification.generateContents(rawJson,lang);
console.log("title:",contents[0])
console.log("body:",contents[1])

const token = fs.readFileSync('./fcm_token.txt', 'utf8');
notification.send(token, contents[0], contents[1])
132 changes: 132 additions & 0 deletions ApiServer/webPushDecipher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
//
// WebPushをdecryptするヤツ(on Node.js)
//
// **参考**
// https://tools.ietf.org/html/rfc8188
// https://tools.ietf.org/html/rfc8291
// https://tools.ietf.org/html/rfc8291#appendix-A
// https://gist.github.com/tateisu/685eab242549d9c9ffc85020f09a4b71
// ↑一部 @tateisu氏のコードを参考にしています
// (アドバイスありがとうございました!→ https://mastodon.juggler.jp/@tateisu/104098620591598243)

const util = require("util");
const crypto = require("crypto");

function decodeBase64(src) {
return Buffer.from(src, "base64");
}

function sha256(key, data) {
return crypto.createHmac("sha256", key).update(data).digest();
}

function log(verbose, label, text) {
if (!verbose) { return; }
console.log(label, text);
}

// 通知を受け取る側で生成したキーを渡す
exports.reciverKeyBuilder = function (public, private, authSecret) {
this.public = decodeBase64(public);
this.private = decodeBase64(private);
this.authSecret = decodeBase64(authSecret);
return this;
};

// WebPushで流れてきた通知をdecrypt
exports.decrypt = function (body64, receiverKey, verbose) {
body = decodeBase64(body64);
auth_secret = receiverKey.authSecret;
receiver_public = receiverKey.public;
receiver_private = receiverKey.private;

// bodyを分解してsalt, keyid, 暗号化されたcontentsを取り出す
// bodyの構造は以下の通り↓
/*
+-----------+--------+-----------+---------------+
| salt (16) | rs (4) | idlen (1) | keyid (idlen) |
+-----------+--------+-----------+---------------+
*/

const salt = body.slice(0, 16);
const rs = body.slice(16, 16 + 4);

const idlen_hex = body.slice(16 + 4, 16 + 4 + 1).toString("hex");
const idlen = parseInt(idlen_hex, 16); // keyidの長さ
const keyid = body.slice(16 + 4 + 1, 16 + 4 + 1 + idlen);

const content = body.slice(16 + 4 + 1 + idlen, body.length);

const sender_public = decodeBase64(keyid.toString("base64"));

// 共有秘密鍵を生成(ECDH)
receiver_curve = crypto.createECDH("prime256v1");
receiver_curve.setPrivateKey(receiver_private);
const sharedSecret = receiver_curve.computeSecret(keyid);

// For Verbose Mode
log(verbose, "salt", salt.toString("base64"));
log(verbose, "rs", rs.toString("hex"));
log(verbose, "idlen_hex", idlen_hex);
log(verbose, "idlen", idlen);
log(verbose, "keyid", keyid.toString("base64"));
log(verbose, "content", content.toString("base64"));
log(verbose, "sender_public", sender_public.toString("base64"));
log(verbose, "sharedSecret:", sharedSecret.toString("base64"));

/*
# HKDF-Extract(salt=auth_secret, IKM=ecdh_secret)
PRK_key = HMAC-SHA-256(auth_secret, ecdh_secret)
# HKDF-Expand(PRK_key, key_info, L_key=32)
key_info = "WebPush: info" || 0x00 || ua_public || as_public
IKM = HMAC-SHA-256(PRK_key, key_info || 0x01)
## HKDF calculations from RFC 8188
# HKDF-Extract(salt, IKM)
PRK = HMAC-SHA-256(salt, IKM)
# HKDF-Expand(PRK, cek_info, L_cek=16)
cek_info = "Content-Encoding: aes128gcm" || 0x00
CEK = HMAC-SHA-256(PRK, cek_info || 0x01)[0..15]
# HKDF-Expand(PRK, nonce_info, L_nonce=12)
nonce_info = "Content-Encoding: nonce" || 0x00
NONCE = HMAC-SHA-256(PRK, nonce_info || 0x01)[0..11]
*/

// key
const prk_key = sha256(auth_secret, sharedSecret);
const keyInfo = Buffer.concat([
Buffer.from("WebPush: info\0"),
receiver_public,
sender_public,
Buffer.from("\1")
]);
const ikm = sha256(prk_key, keyInfo);

// prk
const prk = sha256(salt, ikm);
log(verbose, "prk", prk.toString("base64"));

// cek
const cekInfo = Buffer.from("Content-Encoding: aes128gcm\0\1");
const cek = sha256(prk, cekInfo).slice(0, 16);
log(verbose, "cek", cek.toString("base64"));

// initialization vector
const nonceInfo = Buffer.from("Content-Encoding: nonce\0\1");
const nonce = sha256(prk, nonceInfo).slice(0, 12);
const iv = nonce;
log(verbose, "nonce:", nonce.toString("base64"));

// aes-128-gcm
const decipher = crypto.createDecipheriv("aes-128-gcm", cek, iv);
result = decipher.update(content);
log(verbose, "decrypted: ", result.toString("UTF-8"));

// remove padding and GCM auth tag
while (result.slice(result.length-1,result.length) != "}") { // jsonの末端が見えるまで一文字ずつ消していく
result = result.slice(0,result.length-1);
}

log(verbose, "shaped:", result.toString("UTF-8"));
return result.toString("UTF-8");
};
Loading

0 comments on commit 0ebf818

Please sign in to comment.