-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
67 changed files
with
3,548 additions
and
1,236 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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))); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]:"}} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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= |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
}; |
Oops, something went wrong.