├── .gitignore ├── package.json ├── LICENSE ├── README.md ├── API.md ├── public ├── admin.html └── landing.html └── app.js /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules/ 3 | .DS_Store 4 | switch_to_development.sh 5 | switch_to_production.sh 6 | package-lock.json 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "foreman", 3 | "version": "1.0.0", 4 | "description": "Keystore server for grandmaster", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Dylan Haifisch Laws", 10 | "license": "BSD-3-Clause", 11 | "dependencies": { 12 | "@pm2/io": "^4.3.2", 13 | "basic-auth": "^2.0.1", 14 | "body-parser": "^1.19.0", 15 | "dotenv": "^8.2.0", 16 | "express": "^4.17.1", 17 | "express-useragent": "^1.0.13", 18 | "mongodb": "^3.3.3", 19 | "nodejs-websocket": "^1.7.2", 20 | "shasum": "^1.0.2", 21 | "socket.io": "^2.3.0", 22 | "tsscmp": "^1.0.6", 23 | "uuid": "^3.3.3" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Guardian Firewall 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Foreman 2 | A NodeJS server that acts as an iOS firmware keybag database a.k.a. as a "keystore", serving [Grandmaster](https://github.com/GuardianFirewall/grandmaster) formatted keysets. 3 | 4 | [API Reference](API.md) 5 | ----------------------- 6 | 7 | ## Installation 8 | Tested on a VPS running Ubuntu 18.04. 9 | 10 | ### Install NodeJS 11 | Start by updating apt, installing the prerequisites, and adding the sources to apt. 12 | ``` 13 | sudo apt update 14 | sudo apt -y install curl dirmngr apt-transport-https lsb-release ca-certificates gcc g++ make 15 | curl -sL https://deb.nodesource.com/setup_10.x | sudo bash 16 | ``` 17 | 18 | Now, update apt again and install nodejs. 19 | ``` 20 | sudo apt update 21 | sudo apt -y install nodejs 22 | ``` 23 | 24 | #### PM2 Usage Notice 25 | Foreman leverages [pm2.io](https://pm2.io) for live runtime montioring and providing live metrics for specific endpoints. Foreman allows clients to act with autonomy, anonymously. As such, no PM2 metric will collect any identifier that may link back to a client making requests. 26 | 27 | ### Install mongodb 28 | Simply run the following. 29 | ``` 30 | sudo apt install -y mongodb 31 | ``` 32 | 33 | ### Install Nginx 34 | To install Nginx simply run the following. 35 | ``` 36 | sudo apt update 37 | sudo apt install nginx 38 | ``` 39 | and allow Nginx with ufw. 40 | ``` 41 | ufw allow "Nginx Full" 42 | ``` 43 | 44 | ### Install certbot 45 | To install certbot, using the following commands. 46 | ``` 47 | sudo add-apt-repository ppa:certbot/certbot 48 | sudo apt update 49 | sudo apt-get install certbot 50 | ``` 51 | Generate a certificate to use with Foreman by executing the following. Take note of where it stores your `privkey.pem` and `fullchain.pem` files. 52 | ``` 53 | certbot certonly --standalone --keep-until-expiring --agree-tos -d your_hostname_com 54 | ``` 55 | 56 | ### Configure Nginx 57 | Create a new file named `foreman-server` in `/etc/nginx/sites-available/` and fill it with the following configuration. 58 | 59 | Be sure to modify the `server_name`, `ssl_certificate`, and `ssl_certificate_key` specifiers. 60 | ``` 61 | server { 62 | listen 80 default_server; 63 | server_name _; 64 | return 301 https://$host$request_uri; 65 | } 66 | 67 | server { 68 | listen 443 ssl; 69 | server_name your_hostname_com; 70 | 71 | ssl_certificate path_to_fullchain.pem; 72 | ssl_certificate_key path_to_privkey.pem; 73 | 74 | location / { 75 | proxy_pass https://localhost:4141; 76 | proxy_http_version 1.1; 77 | proxy_set_header Upgrade $http_upgrade; 78 | proxy_set_header Connection 'upgrade'; 79 | proxy_set_header Host $host; 80 | proxy_cache_bypass $http_upgrade; 81 | } 82 | } 83 | ``` 84 | To enable this config, make a symbolic this config in `/etc/nginx/sites-enabled/` and then run `systemctl restart nginx`. 85 | 86 | ### Install Foreman 87 | Move your `cd` to somewhere you'd like foreman to live in and then execute the following. 88 | ``` 89 | git clone https://github.com/GuardianFirewall/foreman.git 90 | cd foreman 91 | npm i 92 | ``` 93 | 94 | Foreman's prerequisites should now be installed. Create a `.env` file in the foreman directory and fill it in with the following replacing values as needed. 95 | ``` 96 | FOREMAN_PORT=4141 97 | FOREMAN_SSL_KEY=path_to_privkey.pem 98 | FOREMAN_SSL_CERT=path_to_fullchain.pem 99 | FOREMAN_ADMIN_DIGEST=SHA512_PASSPHRASE_DIGEST 100 | ``` 101 | 102 | `FOREMAN_DIGEST` is a SHA512 digest of the passphrase you'd like to give the root "foreman" account for the `/admin` interface. 103 | 104 | ## Running Foreman 105 | Execute `node app.js` to start the Foreman server. -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | # Foreman API 2 | 3 | Unauthenticated Endpoints 4 | ------------------------- 5 | 6 | ## GET `/api/queue` 7 | Get all keybags in the decryption queue. 8 | ``` 9 | curl -i -H "Accept: application/json" https://foreman-public.sudosecuritygroup.com/api/queue 10 | ``` 11 | 12 | ## GET `/api/find/all` 13 | Get all available keys in the Foreman keystore. 14 | ``` 15 | curl -i -H "Accept: application/json" https://foreman-public.sudosecuritygroup.com/api/find/all 16 | ``` 17 | 18 | ## GET `/api/find/build/:buildnumber` 19 | Get all keys for a specific iOS build number, ex. 17A860. 20 | ``` 21 | curl -i -H "Accept: application/json" https://foreman-public.sudosecuritygroup.com/api/find/build/17A860 22 | ``` 23 | 24 | ## GET `/api/find/device/:device` 25 | Get all keys for a specific device model, ex. iPod9,1 26 | ``` 27 | curl -i -H "Accept: application/json" https://foreman-public.sudosecuritygroup.com/api/find/device/iPod9,1/ 28 | ``` 29 | 30 | ## GET `/api/find/combo/:device/:build` 31 | Get all keys for a specific device model, ex. iPod9,1 and build number ex. 17A860 32 | ``` 33 | curl -i -H "Accept: application/json" https://foreman-public.sudosecuritygroup.com/api/find/combo/iPod9,1/17A860 34 | ``` 35 | 36 | Authenticated Endpoints 37 | ----------------------- 38 | 39 | ### `NoAuth` Mode Operation 40 | Adding `FOREMAN_FORCE_NOAUTH=true` to your `.env.` configuration will flag the following endpoints to accept any token, making them essentially unauthenticated. *A token and the User-Agent must be present in the request but the token can be set to anything*. 41 | 42 | ## POST `/api/submit/keys` 43 | Submit a grandmaster `gm.config` file to be archived in the keystore. 44 | 45 | Requires that the User-Agent be set to include `grandmaster` along with `x-api-key` being set to an authorized Foreman API token in the request header. 46 | ``` 47 | curl -d '{"build":"17B111","device":"iPod9,1","download":"http://updates-http.cdn-apple.com/2019FallFCS/fullrestores/061-49700/BD7C17D0-0696-11EA-970C-D191B09E16A9/iPodtouch_7_13.2.3_17B111_Restore.ipsw","images":{"Firmware/all_flash/DeviceTree.n112ap.im4p":"","Firmware/all_flash/LLB.n112.RELEASE.im4p":"85784a219eb29bcb1cc862de00a590e7f539c51a7f3403d90c9bdc62490f6b5dab4318f4633269ce3fbbe855b33a4bc7","Firmware/all_flash/iBoot.n112.RELEASE.im4p":"052e13cf2bb7802ba9d1a27046b9f9cf325d957388cd1a4325d114a5b2524391b48111c6d9768ceb29bf0b28bd21ff5c","Firmware/dfu/iBEC.n112.RELEASE.im4p":"9f2f0a3df25594d781052202e09d1a47d4211e5b5864850ee76b0dac53f785148652c17000c5e57b9e2c57040adf2c8e","Firmware/dfu/iBSS.n112.RELEASE.im4p":"e096697bb5ce030cfbe004961dde7f50e384e198e50f1e13ca532016506d71ee176ea87384e3c9e04c9afa7231dbcb4d"},"iosver":"13.2.3","kbags":{"Firmware/all_flash/LLB.n112.RELEASE.im4p":["DEBD6EDC7308203646AE11D4A114E725CF9A1501492B67FDADE7CD7C8A21DA752F0B7D07D6C4F1E90EF8AB10B1EC0215","B15052EB57FA9C6C31E5F0BB67D2B2FE90FD5571DFB8C4F3558B4A6B26FAE4BA3E2333DE4F703F91C0D186F1CE1413B6"],"Firmware/all_flash/iBoot.n112.RELEASE.im4p":["B35F7F51964476895D6B2B5F0015D299CAE2E1A75D7AD664E948E77ACAD52BC785AFDA14307C440B49C0BDAD398B2331","B5BDA4BA78E0E8E99DD74494A613AF7B255E2AA6AF6C21711C8FB7AF8D3BAE95B135585C52EBA1A8A47C1FDFB9ACE8D4"],"Firmware/dfu/iBEC.n112.RELEASE.im4p":["AE4CE9EB184640E992CA576CCCCA8AC4FDB9AD30A1DC07B82175AEAD797F01399947056E6210B61AD1A1AF54084F0D07","9804C011D723156CDA3D5FC96A13015065B1D51203042AE77F2121E9AD7F256A2643DF87057450D90D938A79CF5C4905"],"Firmware/dfu/iBSS.n112.RELEASE.im4p":["FC2F689BBEA2DEA65014931DE81AA985B814A7B0188E50B2DD6A5E37C7E8523150A9E551E1D578D25C96D5FE859FDF74","73CE12C73B2971B412AF50CFFA11DA543AE885AA72336196A78287FA3901D844649F5B66301269A6CBF5596716AE5852"]}}' -H "Content-Type: application/json" -H "x-api-key: GENERATED_TOKEN" -A "grandmaster/0.0.1" -X POST https://foreman-public.sudosecuritygroup.com/api/submit/keys 48 | ``` 49 | 50 | ## POST `/api/submit/keybags` 51 | Submit an unfinished grandmaster `gm.config` file to be added to the keybag decryption queue. 52 | 53 | Requires that the User-Agent be set to include `grandmaster` along with `x-api-key` being set to an authorized Foreman API token in the request header. 54 | ``` 55 | curl -d '{"build":"17B111","device":"iPod9,1","download":"http://updates-http.cdn-apple.com/2019FallFCS/fullrestores/061-49700/BD7C17D0-0696-11EA-970C-D191B09E16A9/iPodtouch_7_13.2.3_17B111_Restore.ipsw","images":{"Firmware/all_flash/LLB.n112.RELEASE.im4p":"85784a219eb29bcb1cc862de00a590e7f539c51a7f3403d90c9bdc62490f6b5dab4318f4633269ce3fbbe855b33a4bc7","Firmware/all_flash/iBoot.n112.RELEASE.im4p":"052e13cf2bb7802ba9d1a27046b9f9cf325d957388cd1a4325d114a5b2524391b48111c6d9768ceb29bf0b28bd21ff5c","Firmware/dfu/iBEC.n112.RELEASE.im4p":"9f2f0a3df25594d781052202e09d1a47d4211e5b5864850ee76b0dac53f785148652c17000c5e57b9e2c57040adf2c8e","Firmware/dfu/iBSS.n112.RELEASE.im4p":"e096697bb5ce030cfbe004961dde7f50e384e198e50f1e13ca532016506d71ee176ea87384e3c9e04c9afa7231dbcb4d"},"iosver":"13.2.3","kbags":{"Firmware/all_flash/LLB.n112.RELEASE.im4p":[],"Firmware/all_flash/iBoot.n112.RELEASE.im4p":[],"Firmware/dfu/iBEC.n112.RELEASE.im4p":[],"Firmware/dfu/iBSS.n112.RELEASE.im4p":[]}}' -H "Content-Type: application/json" -H "x-api-key: GENERATED_TOKEN" -A "grandmaster/0.0.1" -X POST https://foreman-public.sudosecuritygroup.com/api/submit/keybags 56 | ``` -------------------------------------------------------------------------------- /public/admin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Foreman Admin 5 | 6 | 7 | 8 | 9 | 10 | 11 | 23 | 24 | 25 | 26 |
27 | 30 |
31 |
32 |
33 |

Superusers

34 |
35 | 36 | 37 |
38 |
39 | 40 | 41 |
42 | 46 |
47 | 48 |
49 |
50 | 51 | 52 | 53 |
54 | 55 |
56 |
57 |

API Keys

58 | 59 | 60 | 61 | 62 |
63 | 64 | 65 |
66 |
67 |

Keystore Stats

68 | 69 | 70 | 71 |
72 |
73 |
74 |
75 | 76 | 228 | -------------------------------------------------------------------------------- /public/landing.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Foreman Keystore 5 | 6 | 7 | 125 | 126 | 127 | 128 |
129 | 189 |

Foreman

190 |

iOS Firmware Keystore

191 |

192 |
193 |
194 | Reloading Keystore Data 195 |
196 |
197 |
198 | 199 | 362 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const https = require('https') 2 | require('dotenv').config() 3 | const express = require('express') 4 | const path = require('path') 5 | const fs = require('fs'); 6 | const useragent = require('express-useragent') 7 | const bodyParser = require('body-parser'); 8 | const io = require('@pm2/io') 9 | const socket = require('socket.io'); 10 | const uuidv4 = require('uuid/v4'); 11 | const crypto = require('crypto'); 12 | var compare = require('tsscmp'); 13 | var basicAuth = require('basic-auth'); 14 | 15 | var webServer = null; 16 | var socketServer = null; 17 | var socketClients = []; 18 | 19 | var forceNoAuthFlag = ((process.env.FOREMAN_FORCE_NOAUTH == 'true') ? true:false) || false; 20 | 21 | var endpointAllRequestCounter = io.counter({ 22 | name: '/api/find/all requests' 23 | }) 24 | 25 | var endpointBuildRequestCounter = io.counter({ 26 | name: '/api/find/:build requests' 27 | }) 28 | 29 | var endpointDeviceRequestCounter = io.counter({ 30 | name: '/api/find/:device requests' 31 | }) 32 | 33 | var endpointDeviceBuildRequestCounter = io.counter({ 34 | name: '/api/find/:build/:device requests' 35 | }) 36 | 37 | var availableFirmwares = io.metric({ 38 | name: 'Firmwares in Keystore' 39 | }) 40 | 41 | var foremanSubmissionsCounter = io.counter({ 42 | name: 'Submissions' 43 | }) 44 | 45 | // express 46 | const app = express() 47 | const express_port = process.env.FOREMAN_PORT || 4141 48 | 49 | // mongodb 50 | const mongo = require('mongodb').MongoClient 51 | const mongodb_url = 'mongodb://localhost:27017' 52 | 53 | // mongodb globals 54 | var mongo_db = null; 55 | var mongo_collection = null; 56 | var mongo_collection_keybags = null; 57 | var mongo_collection_Superusers = null; 58 | var mongo_collection_APIKeys = null; 59 | function connectToMongo(callback) { 60 | mongo.connect(mongodb_url, { 61 | useNewUrlParser: true, 62 | useUnifiedTopology: true 63 | }, (err, client) => { 64 | if (err) { 65 | console.error(err) 66 | return 67 | } 68 | console.log('connected to mongodb') 69 | mongo_db = client.db('foreman') 70 | mongo_collection = mongo_db.collection('builds') 71 | mongo_collection_keybags = mongo_db.collection('keybags') 72 | mongo_collection_APIKeys = mongo_db.collection('authorizedkeys') 73 | mongo_collection_Superusers = mongo_db.collection('superusers'); 74 | callback() 75 | }) 76 | } 77 | 78 | function mongo_generalCleanConfigForUser(configData, cleanKey) { 79 | var cleanedConfigData = configData 80 | var cleanedImagesDictionary = {} 81 | if (cleanedConfigData['_id'] != undefined) { 82 | delete cleanedConfigData['_id']; 83 | } 84 | let dirtyImagesDictionary = cleanedConfigData[cleanKey] 85 | let dirtyImagesKeys = Object.keys(dirtyImagesDictionary) 86 | for (var i = dirtyImagesKeys.length - 1; i >= 0; i--) { 87 | var cleanKey = dirtyImagesKeys[i].replace(/\_/g,'.') 88 | cleanKey = cleanKey.replace('all.flash', 'all_flash') 89 | cleanedImagesDictionary[cleanKey] = dirtyImagesDictionary[dirtyImagesKeys[i]] 90 | } 91 | cleanedConfigData[cleanKey] = cleanedImagesDictionary 92 | return cleanedConfigData 93 | } 94 | 95 | function mongo_generalCleanConfigForStorageWithKey(configData, cleanKey) { 96 | var cleanedConfigData = configData 97 | var cleanedImagesDictionary = {} 98 | let dirtyImagesDictionary = cleanedConfigData[cleanKey] 99 | let dirtyImagesKeys = Object.keys(dirtyImagesDictionary) 100 | for (var i = dirtyImagesKeys.length - 1; i >= 0; i--) { 101 | let cleanKey = dirtyImagesKeys[i].replace(/\./g,'_') 102 | cleanedImagesDictionary[cleanKey] = dirtyImagesDictionary[dirtyImagesKeys[i]] 103 | } 104 | cleanedConfigData[cleanKey] = cleanedImagesDictionary 105 | return cleanedConfigData 106 | } 107 | 108 | /* Mongo Superuser Collection Methods */ 109 | function mongo_removeSuperuser(superuserProfile, callback) { 110 | console.log(pKey) 111 | if (mongo_collection_Superusers != null) { 112 | mongo_getAllAuthorizedAPIKeys(function(err, allAPIKeys) { 113 | for (var i = allAPIKeys.length - 1; i >= 0; i--) { 114 | if (allAPIKeys[i]['key'] == pKey) { 115 | mongo_collection_Superusers.deleteOne(allAPIKeys[i], callback) 116 | } 117 | } 118 | }) 119 | } 120 | } 121 | 122 | function mongo_addSuperuser(superuserUsername, superuserPassphrase, callback) { 123 | if (mongo_collection_Superusers != null) { 124 | mongo_collection_Superusers.insertOne({'user':superuserUsername, 'hash':superuserPassphrase}, (err, result) => { 125 | if (err) { 126 | callback(err, false) 127 | } else { 128 | callback(err, true) 129 | } 130 | }); 131 | } 132 | } 133 | 134 | function mongo_getSuperusers(callback) { 135 | if (mongo_collection_Superusers != null) { 136 | mongo_collection_Superusers.find().toArray((err, items) => { 137 | callback(err, items) 138 | }) 139 | } 140 | } 141 | 142 | /* Mongo API Key Collection Methods */ 143 | function mongo_removeAuthorizedAPIKey(pKey, callback) { 144 | if (mongo_collection_APIKeys != null) { 145 | mongo_getAllAuthorizedAPIKeys(function(err, allAPIKeys) { 146 | for (var i = allAPIKeys.length - 1; i >= 0; i--) { 147 | if (allAPIKeys[i]['key'] == pKey) { 148 | mongo_collection_APIKeys.deleteOne(allAPIKeys[i], callback) 149 | } 150 | } 151 | }) 152 | } 153 | } 154 | 155 | function mongo_addAuthorizedAPIKey(callback) { 156 | var generatedKey = uuidv4(); 157 | if (mongo_collection_APIKeys != null) { 158 | mongo_collection_APIKeys.insertOne({'key':generatedKey}, (err, result) => { 159 | if (err) { 160 | callback(err, generatedKey) 161 | } else { 162 | callback(err, generatedKey) 163 | } 164 | }) 165 | } 166 | } 167 | 168 | function mongo_getAllAuthorizedAPIKeys(callback) { 169 | if (mongo_collection_APIKeys != null) { 170 | mongo_collection_APIKeys.find().toArray((err, items) => { 171 | callback(err, items) 172 | }) 173 | } 174 | } 175 | 176 | 177 | /* Mongo Keybag Collection Methods */ 178 | function mongo_removeKeybag(configData, callback) { 179 | if (mongo_collection_keybags != null) { 180 | mongo_collection_keybags.deleteOne(configData, callback) 181 | } 182 | } 183 | 184 | function mongo_addKeybag(configData, callback) { 185 | var cleanedConfig = mongo_generalCleanConfigForStorageWithKey(configData, 'kbags') 186 | cleanedConfig = mongo_generalCleanConfigForStorageWithKey(configData, 'images') 187 | if (mongo_collection_keybags != null) { 188 | mongo_collection_keybags.insertOne(cleanedConfig, (err, result) => { 189 | if (err) { 190 | callback(err, result) 191 | } else { 192 | callback(err, result) 193 | } 194 | }) 195 | } 196 | } 197 | 198 | function mongo_getAllKeybags(callback) { 199 | if (mongo_collection_keybags != null) { 200 | mongo_collection_keybags.find().toArray((err, items) => { 201 | if (err) { 202 | callback(err, items) 203 | } else { 204 | availableFirmwares.set(items.length) 205 | var cleanedItems = []; 206 | for (var i = items.length - 1; i >= 0; i--) { 207 | var cleaned = mongo_generalCleanConfigForUser(items[i], 'kbags'); 208 | cleaned = mongo_generalCleanConfigForUser(items[i], 'images') 209 | cleanedItems.push(cleaned) 210 | } 211 | callback(err, cleanedItems) 212 | } 213 | }) 214 | } 215 | } 216 | 217 | /* Mongo Keystore Collection Methods */ 218 | function mongo_addKeystoreConfig(configData, callback) { 219 | let cleanedConfig = mongo_generalCleanConfigForStorageWithKey(configData, 'images') 220 | if (mongo_collection != null) { 221 | mongo_collection.insertOne(cleanedConfig, (err, result) => { 222 | if (err) { 223 | callback(err, result) 224 | } else { 225 | callback(err, result) 226 | } 227 | }) 228 | } 229 | } 230 | 231 | function mongo_getAllConfigsKeystore(callback) { 232 | if (mongo_collection != null) { 233 | mongo_collection.find().toArray((err, items) => { 234 | if (err) { 235 | callback(err, items) 236 | } else { 237 | availableFirmwares.set(items.length) 238 | var cleanedItems = []; 239 | for (var i = items.length - 1; i >= 0; i--) { 240 | cleanedItems.push(mongo_generalCleanConfigForUser(items[i], 'images')) 241 | } 242 | callback(err, cleanedItems) 243 | } 244 | }) 245 | } 246 | } 247 | 248 | // check the keystore for a target device and ios build, if an entry exists return true 249 | function checkKeystoreForDeviceAndBuild(keystore, targetDevice, targetBuild) { 250 | for (var i = keystore.length - 1; i >= 0; i--) { 251 | if (keystore[i]["device"] == targetDevice && keystore[i]["build"] == targetBuild) { 252 | console.log("rejecting submission request for device "+targetDevice+" and build "+targetBuild) 253 | return true; // found a keystore entry 254 | } 255 | } 256 | return false; // no keystore entry found 257 | } 258 | 259 | function validateKeystoreSubmission(requestSubmission) { 260 | try { 261 | // check that our device key is set 262 | if (requestSubmission["device"].length <= 0) { 263 | console.log("failed model validation") 264 | return 1; 265 | } 266 | // check that our build key is set 267 | if (requestSubmission["build"].length <= 0) { 268 | console.log("failed build validation") 269 | return 2; 270 | } 271 | // check that we have images in our submission 272 | let originalImagesDictionary = requestSubmission["images"]; 273 | let requestImagesKeys = Object.keys(originalImagesDictionary) 274 | if (requestImagesKeys.length <= 0) { 275 | console.log("failed image length validation") 276 | return 3; 277 | } 278 | // and check that the keys /look/ valid 279 | for (var i = requestImagesKeys.length - 1; i >= 0; i--) { 280 | let kbag = originalImagesDictionary[requestImagesKeys[i]]; 281 | if (kbag.length != 96) { 282 | console.log("failed key length validation") 283 | return 4; // our key length is invalid 284 | } 285 | } 286 | } catch (error) { 287 | console.log(error) 288 | return 5; 289 | } 290 | 291 | // if all is well, return true. 292 | return 0; 293 | } 294 | 295 | function validateKeybagsSubmission(requestSubmission) { 296 | try { 297 | // check that our device key is set 298 | if (requestSubmission["device"].length <= 0) { 299 | console.log("failed model validation") 300 | return 1; 301 | } 302 | // check that our build key is set 303 | if (requestSubmission["build"].length <= 0) { 304 | console.log("failed build validation") 305 | return 2; 306 | } 307 | // check that we have images in our submission 308 | let originalImagesDictionary = requestSubmission["kbags"]; 309 | let requestImagesKeys = Object.keys(originalImagesDictionary) 310 | if (requestImagesKeys.length <= 0) { 311 | console.log("failed kbags length") 312 | return 3; 313 | } 314 | } catch (error) { 315 | console.log(error) 316 | return 5; 317 | } 318 | 319 | // if all is well, return true. 320 | return 0; 321 | } 322 | 323 | var auth = function (req, res, next) { 324 | var user = basicAuth(req); 325 | if (!user || !user.name || !user.pass) { 326 | res.set('WWW-Authenticate', 'Basic realm=Authorization Required'); 327 | res.sendStatus(401); 328 | return; 329 | } 330 | 331 | if (compare(user.name, 'foreman')) { 332 | let digest = crypto.createHash('sha512').update(user.pass).digest('hex'); 333 | if (compare(digest, process.env.FOREMAN_ADMIN_DIGEST)) { 334 | next(); 335 | return; 336 | } 337 | res.set('WWW-Authenticate', 'Basic realm=Authorization Required'); 338 | res.sendStatus(401); 339 | return; 340 | } 341 | 342 | let givenUsername = user.name; 343 | let givenPassword = user.pass; 344 | if (mongo_collection_Superusers != null) { 345 | mongo_getSuperusers(function(err, allSuperusers) { 346 | var didAuthenticate = false; 347 | for (var i = allSuperusers.length - 1; i >= 0; i--) { 348 | if (compare(user.name, allSuperusers[i].user)) { 349 | let round_one = crypto.createHash('sha256').update(user.pass).digest('hex'); 350 | let round_two = crypto.createHash('sha256').update(user.name+round_one).digest('hex'); 351 | if (round_two == allSuperusers[i].hash) { 352 | didAuthenticate = true; 353 | break; 354 | } 355 | } 356 | } 357 | if (didAuthenticate == false) { 358 | res.set('WWW-Authenticate', 'Basic realm=Authorization Required'); 359 | res.sendStatus(401); 360 | return; 361 | } else { 362 | next(); 363 | return; 364 | } 365 | }); 366 | } else { 367 | res.set('WWW-Authenticate', 'Basic realm=Authorization Required'); 368 | res.sendStatus(401); 369 | return; 370 | } 371 | } 372 | 373 | // json parse for request bodies 374 | app.use(bodyParser.json()); 375 | app.use(bodyParser.urlencoded({ extended: true })); 376 | 377 | // catch any GET requests to our root 378 | app.get('/', (req, res) => res.sendFile(path.join(__dirname, 'public/landing.html'))) 379 | 380 | // catch any GET requests to view our keybag queue 381 | app.get('/api/queue', (req, res) => { 382 | mongo_getAllKeybags(function(err, items) { 383 | res.writeHead(200, {"Content-Type": "application/json"}); 384 | res.end(JSON.stringify(items)); 385 | }); 386 | }); 387 | 388 | // catch any GET requests to our admin root 389 | app.get('/admin', auth, (req, res) => { 390 | res.sendFile(path.join(__dirname, 'public/admin.html')) 391 | }); 392 | 393 | app.get('/admin/apikeys', auth, (req, res) => { 394 | mongo_getAllAuthorizedAPIKeys(function(err, items) { 395 | res.writeHead(200, {"Content-Type": "application/json"}); 396 | res.end(JSON.stringify(items)); 397 | }); 398 | }); 399 | 400 | app.get('/admin/newkey', auth, (req, res) => { 401 | mongo_addAuthorizedAPIKey(function(err, newKey) { 402 | res.writeHead(200, {"Content-Type": "application/json"}); 403 | res.end(JSON.stringify(newKey)); 404 | }); 405 | }); 406 | 407 | app.get('/admin/delete/:apikey', auth, (req, res) => { 408 | mongo_removeAuthorizedAPIKey(req.params.apikey, function(retA, retB) { 409 | res.writeHead(200, {"Content-Type": "application/json"}); 410 | res.end(JSON.stringify(retA)); 411 | }); 412 | }); 413 | 414 | app.get('/admin/superuser/users', auth, (req, res) => { 415 | mongo_getSuperusers(function(err, items) { 416 | res.writeHead(200, {"Content-Type": "application/json"}); 417 | res.end(JSON.stringify(items)); 418 | }); 419 | }); 420 | 421 | app.post('/admin/superuser/add', auth, (req, res) => { 422 | let newSuperuserUsername = req.body.username; 423 | let newSuperuserPassword = req.body.hash; 424 | let hash = crypto.createHash('sha256'); 425 | hash.update(newSuperuserUsername+newSuperuserPassword); 426 | let storedHash = hash.digest('hex'); 427 | mongo_addSuperuser(newSuperuserUsername, storedHash, function(err, didAddSuperuser) { 428 | res.writeHead(200, {"Content-Type": "application/json"}); 429 | res.end(JSON.stringify(didAddSuperuser)); 430 | }); 431 | }); 432 | 433 | app.post('/admin/superuser/remove', auth, (req, res) => { 434 | mongo_removeAuthorizedAPIKey(req.body.apikey, function(retA, retB) { 435 | res.writeHead(200, {"Content-Type": "application/json"}); 436 | res.end(JSON.stringify(retA)); 437 | }); 438 | }); 439 | 440 | app.delete('/queue', (req, res) => { 441 | mongo_removeKeybag(req.body, function(err, ret) { 442 | if (ret) { 443 | console.log("deleting keybag submission"); 444 | res.end(JSON.stringify({"result":true})); 445 | } else { 446 | console.log("error deleting keybag submission, err => "+err); 447 | if (err) { 448 | res.end(JSON.stringify({"result":false, "error":err})); 449 | } else { 450 | res.end(JSON.stringify({"result":false, "error":null})); 451 | } 452 | } 453 | }); 454 | }); 455 | 456 | // handle a GET request for all of our stored keys 457 | app.get('/api/find/all', (req, res) => { 458 | endpointAllRequestCounter.inc() 459 | mongo_getAllConfigsKeystore(function(err, items) { 460 | res.writeHead(200, {"Content-Type": "application/json"}); 461 | res.end(JSON.stringify(items)); 462 | }) 463 | }); 464 | 465 | // handle a GET request for keys, given a target device and build 466 | app.get('/api/find/combo/:device/:build', (req, res) => { 467 | endpointDeviceBuildRequestCounter.inc() 468 | let targetDevice = req.params.device 469 | let targetBuild = req.params.build 470 | mongo_getAllConfigsKeystore(function(err, items) { 471 | let keystore = items 472 | res.writeHead(200, {"Content-Type": "application/json"}); 473 | for (var i = keystore.length - 1; i >= 0; i--) { 474 | if (keystore[i]["device"] == targetDevice && keystore[i]["build"] == targetBuild) { 475 | res.end(JSON.stringify(keystore[i])) // return our found entry 476 | break; // just incase? 477 | } 478 | } 479 | res.end(JSON.stringify({"result":false, "error":"no keystore entries found for "+targetDevice+" and build "+targetBuild})) 480 | }) 481 | }); 482 | 483 | // handle a GET request for all keys available, given a target device 484 | app.get('/api/find/device/:device', (req, res) => { 485 | endpointDeviceRequestCounter.inc() 486 | let targetDevice = req.params.device 487 | if (true) {} 488 | mongo_getAllConfigsKeystore(function(err, items) { 489 | let keystore = items 490 | var foundEntries = [] 491 | for (var i = keystore.length - 1; i >= 0; i--) { 492 | if (keystore[i]["device"] == targetDevice) { 493 | console.log("entry found!") 494 | foundEntries.push(keystore[i]) 495 | } 496 | } 497 | res.writeHead(200, {"Content-Type": "application/json"}); 498 | if (foundEntries.length > 0) { 499 | res.end(JSON.stringify(foundEntries)) 500 | } else { 501 | res.end(JSON.stringify({"result":false, "error":"no keystore entries found for "+targetDevice})) 502 | } 503 | }) 504 | }); 505 | 506 | // handle a GET request for all keys available, given a target build 507 | app.get('/api/find/build/:build', (req, res) => { 508 | endpointBuildRequestCounter.inc() 509 | let targetBuild = req.params.build 510 | mongo_getAllConfigsKeystore(function(err, items) { 511 | let keystore = items 512 | var foundEntries = [] 513 | for (var i = keystore.length - 1; i >= 0; i--) { 514 | if (keystore[i]["build"] == targetBuild) { 515 | console.log("entry found!") 516 | foundEntries.push(keystore[i]) 517 | } 518 | } 519 | res.writeHead(200, {"Content-Type": "application/json"}); 520 | if (foundEntries.length > 0) { 521 | res.end(JSON.stringify(foundEntries)) 522 | } else { 523 | res.end(JSON.stringify({"result":false, "error":"no keystore entries found for "+targetBuild})) 524 | } 525 | }); 526 | }); 527 | 528 | function checkIfKeyIsAuthorized(keyToCheck, callback) { 529 | if (forceNoAuthFlag == true) { 530 | callback(true); 531 | return; 532 | } 533 | mongo_getAllAuthorizedAPIKeys(function(err, allAPIKeys) { 534 | var didFindKey = false; 535 | for (var i = allAPIKeys.length - 1; i >= 0; i--) { 536 | if (allAPIKeys[i]['key'] === keyToCheck) { 537 | didFindKey = true; 538 | } 539 | } 540 | callback(didFindKey); 541 | }); 542 | } 543 | 544 | // handle keybag submission 545 | app.post('/api/submit/keybags', function(req, res) { 546 | console.log("got keybag submission request for device "+req.body.device+" and build "+req.body.build) 547 | let requestUserAgent = req.headers['user-agent']; 548 | let requestToken = req.headers['x-api-key']; 549 | res.writeHead(200, {"Content-Type": "application/json"}); 550 | if (!requestUserAgent.includes("grandmaster")) { 551 | res.end(JSON.stringify({"result":false, "error":"User-Agent is invalid"})) 552 | } 553 | if (requestToken == undefined) { 554 | res.end(JSON.stringify({"result":false, "error":"token is missing"})) 555 | } 556 | checkIfKeyIsAuthorized(requestToken, function(isAuthorized) { 557 | if (isAuthorized) { 558 | // continue on 559 | let submissionBuild = req.body.build 560 | let submissionDevice = req.body.device 561 | mongo_getAllKeybags(function(err, items) { 562 | let keystore = items 563 | if (checkKeystoreForDeviceAndBuild(keystore, submissionDevice, submissionBuild)) { 564 | console.log("keybag submission already exists"); 565 | res.end(JSON.stringify({"result":false, "error":"entry already exists in the keybag queue"})); 566 | } else { 567 | if (validateKeybagsSubmission(req.body) != 0) { 568 | console.log("keybag submission failed validation"); 569 | res.end(JSON.stringify({"result":false, "error":"submission failed validation"})); 570 | } else { 571 | mongo_addKeybag(req.body, function(err, ret){ 572 | if (ret) { 573 | console.log("accepting keybag submission"); 574 | res.end(JSON.stringify({"result":true})); 575 | } else { 576 | console.log("rejecting keybag submission, err => "+err); 577 | if (err) { 578 | res.end(JSON.stringify({"result":false, "error":err})); 579 | } else { 580 | res.end(JSON.stringify({"result":false, "error":null})); 581 | } 582 | } 583 | }); 584 | } 585 | } 586 | }); 587 | } else { 588 | res.end(JSON.stringify({"result":false, "error":"token is not authorized"})) 589 | } 590 | }); 591 | }); 592 | 593 | // handle a key submission 594 | app.post('/api/submit/keys', function(req, res) { 595 | console.log("got keys submission request for device "+req.body.device+" and build "+req.body.build) 596 | let requestUserAgent = req.headers['user-agent']; 597 | let requestToken = req.headers['x-api-key']; 598 | res.writeHead(200, {"Content-Type": "application/json"}); 599 | if (!requestUserAgent.includes("grandmaster")) { 600 | res.end(JSON.stringify({"result":false, "error":"User-Agent is invalid"})) 601 | } 602 | if (requestToken == undefined) { 603 | res.end(JSON.stringify({"result":false, "error":"token is missing"})) 604 | } 605 | 606 | checkIfKeyIsAuthorized(requestToken, function(isAuthorized) { 607 | if (isAuthorized) { 608 | foremanSubmissionsCounter.inc() 609 | // continue on 610 | let submissionBuild = req.body.build 611 | let submissionDevice = req.body.device 612 | mongo_getAllConfigsKeystore(function(err, items) { 613 | let keystore = items 614 | if (checkKeystoreForDeviceAndBuild(keystore, submissionDevice, submissionBuild)) { 615 | res.end(JSON.stringify({"result":false, "error":"entry already exists in the keystore"})); 616 | } else { 617 | if (validateKeystoreSubmission(req.body) != 0) { 618 | res.end(JSON.stringify({"result":false, "error":"submission failed validation"})); 619 | } else { 620 | mongo_addKeystoreConfig(req.body, function(err, ret){ 621 | if (ret) { 622 | socketNewKeysetEmit(req.body); 623 | console.log("accepting submission"); 624 | res.end(JSON.stringify({"result":true})); 625 | } else { 626 | console.log("rejecting submission"); 627 | res.end(JSON.stringify({"result":false, "error":err})); 628 | } 629 | }); 630 | } 631 | } 632 | }); 633 | } else { 634 | res.end(JSON.stringify({"result":false, "error":"token is not authorized"})) 635 | } 636 | }); 637 | }); 638 | 639 | function socketRoutineKeybagEmit() { 640 | console.log('emiting keybag queue to '+((socketClients.length == undefined) ? 0:socketClients.length)+' clients.'); 641 | mongo_getAllKeybags(function(err, items) { 642 | for (var i = socketClients.length - 1; i >= 0; i--) { 643 | if (socketClients[i] != undefined) { 644 | console.log(JSON.stringify(items)) 645 | socketClients[i].emit('queue', items); 646 | } 647 | } 648 | }); 649 | } 650 | 651 | function socketRoutineHeartbeatEmit() { 652 | console.log('emiting a heartbeat to '+((socketClients.length == undefined) ? 0:socketClients.length)+' clients.'); 653 | for (var i = socketClients.length - 1; i >= 0; i--) { 654 | if (socketClients[i] != undefined) { 655 | let epochNow = Math.floor(new Date() / 1000); 656 | socketClients[i].emit('heartbeat', epochNow); 657 | } 658 | } 659 | } 660 | 661 | function socketNewKeysetEmit(keyset) { 662 | console.log('emiting new keyset to '+((socketClients.length == undefined) ? 0:socketClients.length)+' clients.'); 663 | for (var i = socketClients.length - 1; i >= 0; i--) { 664 | if (socketClients[i] != undefined) { 665 | var cleanKeyset = keyset; 666 | if (cleanKeyset['_id'] != undefined) { 667 | delete cleanKeyset['_id']; 668 | } 669 | socketClients[i].emit('keyset', cleanKeyset); 670 | } 671 | } 672 | } 673 | 674 | // connect to mongo 675 | connectToMongo(function() { 676 | // reload the keystore 677 | mongo_getAllConfigsKeystore(function(err, items) { 678 | console.log("reloaded keystore") 679 | }) 680 | // create our https API server 681 | var certOptions = { 682 | key: fs.readFileSync(process.env.FOREMAN_SSL_KEY), 683 | cert: fs.readFileSync(process.env.FOREMAN_SSL_CERT) 684 | }; 685 | webServer = https.createServer(certOptions, app).listen(express_port, function () { 686 | console.log('foreman listening on port '+express_port) 687 | }); 688 | socketServer = socket(webServer) 689 | socketServer.on('connection', function(client) { 690 | console.log('client connected'); 691 | mongo_getAllKeybags(function(err, items) { 692 | console.log(JSON.stringify(items)); 693 | client.emit('queue', items); 694 | }); 695 | socketClients.push(client); 696 | client.on('clientheartbeat', function(data) { 697 | if (data) { 698 | console.log('got a clientheartbeat => '+(new Date(data*1000))); 699 | } 700 | }); 701 | client.on('queue', function(ack) { 702 | mongo_getAllKeybags(function(err, items) { 703 | console.log(JSON.stringify(items)) 704 | ack(items); 705 | }); 706 | }); 707 | client.on('disconnect', function() { 708 | console.log('client disconnected'); 709 | var i = socketClients.indexOf(client); 710 | socketClients.splice(i, 1); 711 | }); 712 | }); 713 | // routine emit a heartbeat every 1 minute 714 | setInterval(socketRoutineHeartbeatEmit, 60000); 715 | // routine emit the keybag queue every 15 minutes 716 | setInterval(socketRoutineKeybagEmit, 900000); 717 | }); 718 | --------------------------------------------------------------------------------