forked from mozilla/blurts-server
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathhibp.js
200 lines (170 loc) · 5.97 KB
/
hibp.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
"use strict";
const got = require("got");
const AppConstants = require("./app-constants");
const { FluentError } = require("./locale-utils");
const mozlog = require("./log");
const pkg = require("./package.json");
const HIBP_USER_AGENT = `${pkg.name}/${pkg.version}`;
// When HIBP "re-names" a breach, it keeps its old 'Name' value but gets a new 'Title'
// We use 'Name' in Firefox (via Remote Settings), so we have to maintain our own mapping of re-named breaches.
const RENAMED_BREACHES = ["covve"];
const RENAMED_BREACHES_MAP = {
"covve": "db8151dd",
};
const log = mozlog("hibp");
const HIBP = {
_addStandardOptions (options = {}) {
const hibpOptions = {
headers: {
"User-Agent": HIBP_USER_AGENT,
},
json: true,
};
return Object.assign(options, hibpOptions);
},
async _throttledGot (url, reqOptions, tryCount = 1) {
let response;
try {
response = await got(url, reqOptions);
return response;
} catch (err) {
log.error("_throttledGot", {err: err});
if (err.statusCode === 404) {
// 404 can mean "no results", return undefined response; sorry calling code
return response;
} else if (err.statusCode === 429) {
log.info("_throttledGot", {err: "got a 429, tryCount: " + tryCount});
if (tryCount >= AppConstants.HIBP_THROTTLE_MAX_TRIES) {
log.error("_throttledGot", {err: err});
throw new FluentError("error-hibp-throttled");
} else {
tryCount++;
await new Promise(resolve => setTimeout(resolve, AppConstants.HIBP_THROTTLE_DELAY * tryCount));
return await this._throttledGot(url, reqOptions, tryCount);
}
} else {
throw new FluentError("error-hibp-connect");
}
}
},
async req(path, options = {}) {
const url = `${AppConstants.HIBP_API_ROOT}${path}`;
const reqOptions = this._addStandardOptions(options);
return await this._throttledGot(url, reqOptions);
},
async kAnonReq(path, options = {}) {
// Construct HIBP url and standard headers
const url = `${AppConstants.HIBP_KANON_API_ROOT}${path}?code=${encodeURIComponent(AppConstants.HIBP_KANON_API_TOKEN)}`;
const reqOptions = this._addStandardOptions(options);
return await this._throttledGot(url, reqOptions);
},
matchFluentID(dataCategory) {
return dataCategory.toLowerCase()
.replace(/[^-a-z0-9]/g, "-")
.replace(/-{2,}/g, "-")
.replace(/(^-|-$)/g, "");
},
formatDataClassesArray(dataCategories) {
const formattedArray = [];
dataCategories.forEach(category => {
formattedArray.push(this.matchFluentID(category));
});
return formattedArray;
},
async loadBreachesIntoApp(app) {
log.info("loadBreachesIntoApp");
try {
const breachesResponse = await this.req("/breaches");
const breaches = [];
for (const breach of breachesResponse.body) {
breach.DataClasses = this.formatDataClassesArray(breach.DataClasses);
breach.LogoPath = /[^/]*$/.exec(breach.LogoPath)[0];
breaches.push(breach);
}
app.locals.breaches = breaches;
app.locals.breachesLoadedDateTime = Date.now();
app.locals.latestBreach = this.getLatestBreach(breaches);
app.locals.mostRecentBreachDateTime = app.locals.latestBreach.AddedDate;
} catch (error) {
throw new FluentError("error-hibp-load-breaches");
}
log.info("done-loading-breaches");
},
async getBreachesForEmail(sha1, allBreaches, includeSensitive = false, filterBreaches = true) {
let foundBreaches = [];
const sha1Prefix = sha1.slice(0, 6).toUpperCase();
const path = `/breachedaccount/range/${sha1Prefix}`;
const response = await this.kAnonReq(path);
if (!response) {
return [];
}
// Parse response body, format:
// [
// {"hashSuffix":<suffix>,"websites":[<breach1Name>,...]},
// {"hashSuffix":<suffix>,"websites":[<breach1Name>,...]},
// ]
for (const breachedAccount of response.body) {
if (sha1.toUpperCase() === sha1Prefix + breachedAccount.hashSuffix) {
foundBreaches = allBreaches.filter(breach => breachedAccount.websites.includes(breach.Name));
if (filterBreaches) {
foundBreaches = this.filterBreaches(foundBreaches);
}
// NOTE: DO NOT CHANGE THIS SORT LOGIC
// We store breach resolutions by recency indices,
// so that our DB does not contain any part of any user's list of accounts
foundBreaches.sort( (a,b) => {
return new Date(b.AddedDate) - new Date(a.AddedDate);
});
break;
}
}
if (includeSensitive) {
return foundBreaches;
}
return foundBreaches.filter(
breach => !breach.IsSensitive
);
},
getBreachByName(allBreaches, breachName) {
breachName = breachName.toLowerCase();
if (RENAMED_BREACHES.includes(breachName)) {
breachName = RENAMED_BREACHES_MAP[breachName];
}
const foundBreach = allBreaches.find(breach => breach.Name.toLowerCase() === breachName);
return foundBreach;
},
filterBreaches(breaches) {
return breaches.filter(
breach => !breach.IsRetired &&
!breach.IsSpamList &&
!breach.IsFabricated &&
breach.IsVerified &&
breach.Domain !== ""
);
},
getLatestBreach(breaches) {
let latestBreach = {};
let latestBreachDateTime = new Date(0);
for (const breach of breaches) {
if (breach.IsSensitive) {
continue;
}
const breachAddedDate = new Date(breach.AddedDate);
if (breachAddedDate > latestBreachDateTime) {
latestBreachDateTime = breachAddedDate;
latestBreach = breach;
}
}
return latestBreach;
},
async subscribeHash(sha1) {
const sha1Prefix = sha1.slice(0, 6).toUpperCase();
const path = "/range/subscribe";
const options = {
method: "POST",
body: {hashPrefix: sha1Prefix},
};
return await this.kAnonReq(path, options);
},
};
module.exports = HIBP;