@prosopo/provider 3.2.5 → 3.12.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +578 -0
- package/dist/api/admin/apiRemoveDetectorKeyEndpoint.js +7 -4
- package/dist/api/blacklistRequestInspector.js +26 -20
- package/dist/api/captcha.js +121 -33
- package/dist/api/domainMiddleware.js +8 -8
- package/dist/api/headerCheckMiddleware.js +4 -0
- package/dist/api/ignoreMiddleware.js +4 -1
- package/dist/api/ja4Middleware.js +5 -23
- package/dist/api/public.js +26 -3
- package/dist/api/verify.js +4 -2
- package/dist/cjs/api/admin/apiRemoveDetectorKeyEndpoint.cjs +6 -3
- package/dist/cjs/api/blacklistRequestInspector.cjs +25 -19
- package/dist/cjs/api/captcha.cjs +121 -33
- package/dist/cjs/api/domainMiddleware.cjs +8 -8
- package/dist/cjs/api/headerCheckMiddleware.cjs +4 -0
- package/dist/cjs/api/ignoreMiddleware.cjs +3 -0
- package/dist/cjs/api/ja4Middleware.cjs +4 -22
- package/dist/cjs/api/public.cjs +26 -3
- package/dist/cjs/api/verify.cjs +4 -2
- package/dist/cjs/compositeIpAddress.cjs +53 -0
- package/dist/cjs/index.cjs +7 -0
- package/dist/cjs/pairs.cjs +27 -0
- package/dist/cjs/services/ipComparison.cjs +123 -0
- package/dist/cjs/services/ipInfo.cjs +87 -0
- package/dist/cjs/tasks/captchaManager.cjs +49 -2
- package/dist/cjs/tasks/client/clientTasks.cjs +33 -12
- package/dist/cjs/tasks/detection/decodePayload.cjs +712 -278
- package/dist/cjs/tasks/detection/getBotScore.cjs +14 -3
- package/dist/cjs/tasks/frictionless/frictionlessTasks.cjs +128 -24
- package/dist/cjs/tasks/frictionless/frictionlessTasksUtils.cjs +17 -0
- package/dist/cjs/tasks/imgCaptcha/imgCaptchaTasks.cjs +62 -20
- package/dist/cjs/tasks/powCaptcha/powTasks.cjs +43 -15
- package/dist/cjs/util.cjs +248 -16
- package/dist/cjs/utils/hashUserAgent.cjs +10 -0
- package/dist/compositeIpAddress.js +53 -0
- package/dist/index.js +8 -1
- package/dist/pairs.js +27 -0
- package/dist/services/ipComparison.js +123 -0
- package/dist/services/ipInfo.js +87 -0
- package/dist/tasks/captchaManager.js +49 -2
- package/dist/tasks/client/clientTasks.js +33 -12
- package/dist/tasks/detection/decodePayload.js +712 -278
- package/dist/tasks/detection/getBotScore.js +15 -4
- package/dist/tasks/frictionless/frictionlessTasks.js +128 -24
- package/dist/tasks/frictionless/frictionlessTasksUtils.js +18 -1
- package/dist/tasks/imgCaptcha/imgCaptchaTasks.js +64 -22
- package/dist/tasks/powCaptcha/powTasks.js +44 -16
- package/dist/util.js +249 -17
- package/dist/utils/hashUserAgent.js +10 -0
- package/package.json +28 -25
- package/vite.test.config.ts +3 -2
- package/vite.threads.test.config.ts +33 -0
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const decodePayload = require("./decodePayload.cjs");
|
|
4
|
+
const DEFAULT_ENTROPY = 13837;
|
|
4
5
|
const getBotScore = async (payload, privateKeyString) => {
|
|
5
|
-
const result = await decodePayload(
|
|
6
|
+
const result = await decodePayload(
|
|
7
|
+
payload,
|
|
8
|
+
privateKeyString
|
|
9
|
+
);
|
|
6
10
|
const baseBotScore = result.score;
|
|
7
11
|
const timestamp = result.timestamp;
|
|
12
|
+
const providerSelectEntropy = result.providerSelectEntropy;
|
|
13
|
+
const userId = result.userId;
|
|
14
|
+
const userAgent = result.userAgent;
|
|
8
15
|
if (baseBotScore === void 0) {
|
|
9
|
-
return {
|
|
16
|
+
return {
|
|
17
|
+
baseBotScore: 1,
|
|
18
|
+
timestamp: 0,
|
|
19
|
+
providerSelectEntropy: DEFAULT_ENTROPY
|
|
20
|
+
};
|
|
10
21
|
}
|
|
11
|
-
return { baseBotScore, timestamp };
|
|
22
|
+
return { baseBotScore, timestamp, providerSelectEntropy, userId, userAgent };
|
|
12
23
|
};
|
|
13
24
|
exports.getBotScore = getBotScore;
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const loadBalancer = require("@prosopo/load-balancer");
|
|
3
4
|
const types = require("@prosopo/types");
|
|
4
5
|
const uuid = require("uuid");
|
|
5
6
|
const lang = require("../../rules/lang.cjs");
|
|
6
7
|
const captchaManager = require("../captchaManager.cjs");
|
|
7
8
|
const getBotScore = require("../detection/getBotScore.cjs");
|
|
9
|
+
const getDefaultEntropy = () => {
|
|
10
|
+
if (process.env.PROSOPO_ENTROPY) {
|
|
11
|
+
const parsed = Number.parseInt(process.env.PROSOPO_ENTROPY);
|
|
12
|
+
if (!Number.isNaN(parsed)) {
|
|
13
|
+
return parsed;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return 13337;
|
|
17
|
+
};
|
|
8
18
|
const DEFAULT_MAX_TIMESTAMP_AGE = 60 * 10 * 1e3;
|
|
19
|
+
const DEFAULT_ENTROPY = getDefaultEntropy();
|
|
9
20
|
class FrictionlessManager extends captchaManager.CaptchaManager {
|
|
10
21
|
constructor(db, pair, config, logger) {
|
|
11
22
|
super(db, pair, logger);
|
|
@@ -14,26 +25,54 @@ class FrictionlessManager extends captchaManager.CaptchaManager {
|
|
|
14
25
|
checkLangRules(acceptLanguage) {
|
|
15
26
|
return lang.checkLangRules(this.config, acceptLanguage);
|
|
16
27
|
}
|
|
17
|
-
async createSession(tokenId, captchaType) {
|
|
28
|
+
async createSession(tokenId, captchaType, solvedImagesCount, powDifficulty) {
|
|
18
29
|
const sessionRecord = {
|
|
19
30
|
sessionId: uuid.v4(),
|
|
20
31
|
createdAt: /* @__PURE__ */ new Date(),
|
|
21
32
|
tokenId,
|
|
22
|
-
captchaType
|
|
33
|
+
captchaType,
|
|
34
|
+
solvedImagesCount,
|
|
35
|
+
powDifficulty
|
|
23
36
|
};
|
|
24
37
|
await this.db.storeSessionRecord(sessionRecord);
|
|
25
38
|
return sessionRecord;
|
|
26
39
|
}
|
|
27
|
-
async
|
|
28
|
-
const
|
|
40
|
+
async hostVerified(entropy) {
|
|
41
|
+
const chosen = await loadBalancer.getRandomActiveProvider(
|
|
42
|
+
this.config.defaultEnvironment,
|
|
43
|
+
entropy
|
|
44
|
+
);
|
|
45
|
+
const domain = new URL(chosen.provider.url).hostname;
|
|
46
|
+
this.logger.info(() => ({
|
|
47
|
+
data: { entropy, host: this.config.host, domain }
|
|
48
|
+
}));
|
|
49
|
+
if (domain !== this.config.host) {
|
|
50
|
+
this.logger.info(() => ({
|
|
51
|
+
msg: "Host mismatch",
|
|
52
|
+
data: { expected: this.config.host, got: domain, entropy }
|
|
53
|
+
}));
|
|
54
|
+
return { verified: false, domain };
|
|
55
|
+
}
|
|
56
|
+
return { verified: true, domain };
|
|
57
|
+
}
|
|
58
|
+
async sendImageCaptcha(tokenId, solvedImagesCount) {
|
|
59
|
+
const sessionRecord = await this.createSession(
|
|
60
|
+
tokenId,
|
|
61
|
+
types.CaptchaType.image,
|
|
62
|
+
solvedImagesCount
|
|
63
|
+
);
|
|
29
64
|
return {
|
|
30
65
|
[types.ApiParams.captchaType]: types.CaptchaType.image,
|
|
31
66
|
[types.ApiParams.sessionId]: sessionRecord.sessionId,
|
|
32
67
|
[types.ApiParams.status]: "ok"
|
|
33
68
|
};
|
|
34
69
|
}
|
|
35
|
-
async sendPowCaptcha(tokenId) {
|
|
36
|
-
const sessionRecord = await this.createSession(
|
|
70
|
+
async sendPowCaptcha(tokenId, powDifficulty) {
|
|
71
|
+
const sessionRecord = await this.createSession(
|
|
72
|
+
tokenId,
|
|
73
|
+
types.CaptchaType.pow,
|
|
74
|
+
powDifficulty
|
|
75
|
+
);
|
|
37
76
|
return {
|
|
38
77
|
[types.ApiParams.captchaType]: types.CaptchaType.pow,
|
|
39
78
|
[types.ApiParams.sessionId]: sessionRecord.sessionId,
|
|
@@ -52,6 +91,21 @@ class FrictionlessManager extends captchaManager.CaptchaManager {
|
|
|
52
91
|
});
|
|
53
92
|
return botScore;
|
|
54
93
|
}
|
|
94
|
+
async scoreIncreaseUnverifiedHost(host, baseBotScore, botScore, tokenId) {
|
|
95
|
+
this.logger.info(() => ({
|
|
96
|
+
msg: "Host not verified",
|
|
97
|
+
data: { requested: this.config.host, selected: host }
|
|
98
|
+
}));
|
|
99
|
+
botScore += this.config.penalties.PENALTY_UNVERIFIED_HOST;
|
|
100
|
+
await this.db.updateFrictionlessTokenRecord(tokenId, {
|
|
101
|
+
score: botScore,
|
|
102
|
+
scoreComponents: {
|
|
103
|
+
baseScore: baseBotScore,
|
|
104
|
+
unverifiedHost: this.config.penalties.PENALTY_UNVERIFIED_HOST
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
return botScore;
|
|
108
|
+
}
|
|
55
109
|
async scoreIncreaseTimestamp(timestamp, baseBotScore, botScore, tokenId) {
|
|
56
110
|
this.logger.info(() => ({
|
|
57
111
|
msg: "Timestamp is older than 10 minutes",
|
|
@@ -72,22 +126,31 @@ class FrictionlessManager extends captchaManager.CaptchaManager {
|
|
|
72
126
|
const diff = now - timestamp;
|
|
73
127
|
return diff > DEFAULT_MAX_TIMESTAMP_AGE;
|
|
74
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Redacts a key for logging purposes by showing only the first 5, middle 10, and last 5 characters
|
|
131
|
+
* @param key - The key to redact
|
|
132
|
+
* @returns Redacted key string or empty string if key is falsy
|
|
133
|
+
*/
|
|
134
|
+
redactKeyForLogging(key) {
|
|
135
|
+
if (!key) return "";
|
|
136
|
+
const start = key.slice(0, 5);
|
|
137
|
+
const middle = key.slice(
|
|
138
|
+
Math.floor(key.length / 2) - 5,
|
|
139
|
+
Math.floor(key.length / 2) + 5
|
|
140
|
+
);
|
|
141
|
+
const end = key.slice(-5);
|
|
142
|
+
return `${start}...${middle}...${end}`;
|
|
143
|
+
}
|
|
75
144
|
async decryptPayload(token) {
|
|
76
145
|
const decryptKeys = [
|
|
77
|
-
|
|
78
|
-
...await this.getDetectorKeys()
|
|
146
|
+
// Process DB keys first, then env var key last as env key will likely be invalid
|
|
147
|
+
...await this.getDetectorKeys(),
|
|
148
|
+
process.env.BOT_DECRYPTION_KEY
|
|
79
149
|
].filter((k) => k);
|
|
80
150
|
this.logger.debug(() => {
|
|
81
|
-
const loggedKeys = decryptKeys.map(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const middle = key.slice(
|
|
85
|
-
Math.floor(key.length / 2) - 5,
|
|
86
|
-
Math.floor(key.length / 2) + 5
|
|
87
|
-
);
|
|
88
|
-
const end = key.slice(-5);
|
|
89
|
-
return `${start}...${middle}...${end}`;
|
|
90
|
-
});
|
|
151
|
+
const loggedKeys = decryptKeys.map(
|
|
152
|
+
(key) => this.redactKeyForLogging(key)
|
|
153
|
+
);
|
|
91
154
|
return {
|
|
92
155
|
msg: "Decrypting score",
|
|
93
156
|
data: {
|
|
@@ -98,19 +161,39 @@ class FrictionlessManager extends captchaManager.CaptchaManager {
|
|
|
98
161
|
});
|
|
99
162
|
let baseBotScore;
|
|
100
163
|
let timestamp;
|
|
164
|
+
let providerSelectEntropy;
|
|
165
|
+
let userId;
|
|
166
|
+
let userAgent;
|
|
101
167
|
for (const [keyIndex, key] of decryptKeys.entries()) {
|
|
102
168
|
try {
|
|
103
|
-
const { baseBotScore: s, timestamp: t } = await getBotScore.getBotScore(token, key);
|
|
104
169
|
this.logger.info(() => ({
|
|
170
|
+
msg: "Attempting to decrypt score",
|
|
171
|
+
data: {
|
|
172
|
+
key: this.redactKeyForLogging(key)
|
|
173
|
+
}
|
|
174
|
+
}));
|
|
175
|
+
const decrypted = await getBotScore.getBotScore(token, key);
|
|
176
|
+
const s = decrypted.baseBotScore;
|
|
177
|
+
const t = decrypted.timestamp;
|
|
178
|
+
const p = decrypted.providerSelectEntropy;
|
|
179
|
+
const a = decrypted.userId;
|
|
180
|
+
const u = decrypted.userAgent;
|
|
181
|
+
this.logger.debug(() => ({
|
|
105
182
|
msg: "Successfully decrypted score",
|
|
106
183
|
data: {
|
|
107
|
-
key:
|
|
184
|
+
key: this.redactKeyForLogging(key),
|
|
108
185
|
baseBotScore: s,
|
|
109
|
-
timestamp: t
|
|
186
|
+
timestamp: t,
|
|
187
|
+
entropy: p,
|
|
188
|
+
userId: a,
|
|
189
|
+
userAgent: u
|
|
110
190
|
}
|
|
111
191
|
}));
|
|
112
192
|
baseBotScore = s;
|
|
113
193
|
timestamp = t;
|
|
194
|
+
providerSelectEntropy = p;
|
|
195
|
+
userId = a;
|
|
196
|
+
userAgent = u;
|
|
114
197
|
break;
|
|
115
198
|
} catch (err) {
|
|
116
199
|
if (keyIndex === decryptKeys.length - 1) {
|
|
@@ -119,17 +202,38 @@ class FrictionlessManager extends captchaManager.CaptchaManager {
|
|
|
119
202
|
}));
|
|
120
203
|
baseBotScore = 1;
|
|
121
204
|
timestamp = 0;
|
|
205
|
+
providerSelectEntropy = DEFAULT_ENTROPY + 1;
|
|
122
206
|
}
|
|
123
207
|
}
|
|
124
208
|
}
|
|
125
|
-
|
|
209
|
+
const baseBotScoreUndefined = baseBotScore === void 0;
|
|
210
|
+
const timestampUndefined = timestamp === void 0;
|
|
211
|
+
const providerSelectEntropyUndefined = providerSelectEntropy === void 0;
|
|
212
|
+
const undefinedCount = Number(baseBotScoreUndefined) + Number(timestampUndefined) + Number(providerSelectEntropyUndefined);
|
|
213
|
+
if (undefinedCount > 0) {
|
|
126
214
|
this.logger.error(() => ({
|
|
127
|
-
msg: "Error decrypting score: baseBotScore or timestamp is undefined"
|
|
215
|
+
msg: "Error decrypting score: baseBotScore or timestamp or providerSelectEntropy is undefined"
|
|
128
216
|
}));
|
|
129
217
|
baseBotScore = 1;
|
|
130
218
|
timestamp = 0;
|
|
219
|
+
providerSelectEntropy = DEFAULT_ENTROPY - undefinedCount;
|
|
131
220
|
}
|
|
132
|
-
|
|
221
|
+
this.logger.info(() => ({
|
|
222
|
+
msg: "decryptPayload result",
|
|
223
|
+
data: {
|
|
224
|
+
baseBotScore,
|
|
225
|
+
timestamp,
|
|
226
|
+
entropy: providerSelectEntropy
|
|
227
|
+
}
|
|
228
|
+
}));
|
|
229
|
+
return {
|
|
230
|
+
baseBotScore: Number(baseBotScore),
|
|
231
|
+
timestamp: Number(timestamp),
|
|
232
|
+
providerSelectEntropy: Number(providerSelectEntropy),
|
|
233
|
+
userId,
|
|
234
|
+
userAgent
|
|
235
|
+
};
|
|
133
236
|
}
|
|
134
237
|
}
|
|
238
|
+
exports.DEFAULT_ENTROPY = DEFAULT_ENTROPY;
|
|
135
239
|
exports.FrictionlessManager = FrictionlessManager;
|
|
@@ -8,4 +8,21 @@ const computeFrictionlessScore = (scoreComponents) => {
|
|
|
8
8
|
).toFixed(2)
|
|
9
9
|
);
|
|
10
10
|
};
|
|
11
|
+
const timestampDecayFunction = (timestamp) => {
|
|
12
|
+
const max = (/* @__PURE__ */ new Date()).getTime();
|
|
13
|
+
if (max - timestamp > 36e5) {
|
|
14
|
+
return 12;
|
|
15
|
+
}
|
|
16
|
+
const min = 1e3;
|
|
17
|
+
const age = max - timestamp;
|
|
18
|
+
const decay = Math.log10(2e3) / max;
|
|
19
|
+
const bigScore = max * (1 - (1 - Math.exp(decay * age) ** 24));
|
|
20
|
+
return Math.max(
|
|
21
|
+
2,
|
|
22
|
+
Math.round(
|
|
23
|
+
(Math.log(bigScore) - Math.log(min)) / (Math.log(max) - Math.log(min)) * 2.5
|
|
24
|
+
)
|
|
25
|
+
);
|
|
26
|
+
};
|
|
11
27
|
exports.computeFrictionlessScore = computeFrictionlessScore;
|
|
28
|
+
exports.timestampDecayFunction = timestampDecayFunction;
|
|
@@ -6,6 +6,8 @@ const datasets = require("@prosopo/datasets");
|
|
|
6
6
|
const types = require("@prosopo/types");
|
|
7
7
|
const util$2 = require("@prosopo/util");
|
|
8
8
|
const utilCrypto = require("@prosopo/util-crypto");
|
|
9
|
+
const compositeIpAddress = require("../../compositeIpAddress.cjs");
|
|
10
|
+
const pairs = require("../../pairs.cjs");
|
|
9
11
|
const lang = require("../../rules/lang.cjs");
|
|
10
12
|
const util = require("../../util.cjs");
|
|
11
13
|
const captchaManager = require("../captchaManager.cjs");
|
|
@@ -78,7 +80,7 @@ class ImgCaptchaManager extends captchaManager.CaptchaManager {
|
|
|
78
80
|
salt,
|
|
79
81
|
deadlineTs,
|
|
80
82
|
currentTime,
|
|
81
|
-
|
|
83
|
+
compositeIpAddress.getCompositeIpAddress(ipAddress),
|
|
82
84
|
threshold,
|
|
83
85
|
frictionlessTokenId
|
|
84
86
|
);
|
|
@@ -100,7 +102,7 @@ class ImgCaptchaManager extends captchaManager.CaptchaManager {
|
|
|
100
102
|
* @param providerRequestHashSignature
|
|
101
103
|
* @param ipAddress
|
|
102
104
|
* @param headers
|
|
103
|
-
* @param
|
|
105
|
+
* @param ja4
|
|
104
106
|
* @return {Promise<DappUserSolutionResult>} result containing the contract event
|
|
105
107
|
*/
|
|
106
108
|
async dappUserSolution(userAccount, dappAccount, requestHash, captchas, userTimestampSignature, timestamp, providerRequestHashSignature, ipAddress, headers, ja4) {
|
|
@@ -152,6 +154,8 @@ class ImgCaptchaManager extends captchaManager.CaptchaManager {
|
|
|
152
154
|
);
|
|
153
155
|
if (pendingRequest) {
|
|
154
156
|
const { storedCaptchas, receivedCaptchas, captchaIds } = await this.validateReceivedCaptchasAgainstStoredCaptchas(captchas);
|
|
157
|
+
const flat = receivedCaptchas.map((c) => util$2.extractData(c.salt));
|
|
158
|
+
const pairs$1 = flat.map((list) => pairs.constructPairList(list));
|
|
155
159
|
const { tree, commitmentId } = imgCaptchaTasksUtils.buildTreeAndGetCommitmentId(receivedCaptchas);
|
|
156
160
|
const datasetId = util$2.at(storedCaptchas, 0).datasetId;
|
|
157
161
|
if (!datasetId) {
|
|
@@ -171,7 +175,7 @@ class ImgCaptchaManager extends captchaManager.CaptchaManager {
|
|
|
171
175
|
userSubmitted: true,
|
|
172
176
|
serverChecked: false,
|
|
173
177
|
requestedAtTimestamp: timestamp,
|
|
174
|
-
ipAddress,
|
|
178
|
+
ipAddress: compositeIpAddress.getCompositeIpAddress(ipAddress),
|
|
175
179
|
headers,
|
|
176
180
|
frictionlessTokenId: pendingRecord.frictionlessTokenId,
|
|
177
181
|
ja4
|
|
@@ -191,6 +195,21 @@ class ImgCaptchaManager extends captchaManager.CaptchaManager {
|
|
|
191
195
|
})
|
|
192
196
|
);
|
|
193
197
|
const totalImages = storedCaptchas[0]?.items.length || 0;
|
|
198
|
+
if (pairs.containsIdenticalPairs(pairs$1)) {
|
|
199
|
+
await this.db.disapproveDappUserCommitment(
|
|
200
|
+
commitmentId,
|
|
201
|
+
"CAPTCHA.INVALID_SOLUTION",
|
|
202
|
+
pairs$1
|
|
203
|
+
);
|
|
204
|
+
response = {
|
|
205
|
+
captchas: captchaIds.map((id) => ({
|
|
206
|
+
captchaId: id,
|
|
207
|
+
proof: [[]]
|
|
208
|
+
})),
|
|
209
|
+
verified: false
|
|
210
|
+
};
|
|
211
|
+
return response;
|
|
212
|
+
}
|
|
194
213
|
if (datasets.compareCaptchaSolutions(
|
|
195
214
|
receivedCaptchas,
|
|
196
215
|
solutionRecords,
|
|
@@ -204,11 +223,12 @@ class ImgCaptchaManager extends captchaManager.CaptchaManager {
|
|
|
204
223
|
})),
|
|
205
224
|
verified: true
|
|
206
225
|
};
|
|
207
|
-
await this.db.approveDappUserCommitment(commitmentId);
|
|
226
|
+
await this.db.approveDappUserCommitment(commitmentId, pairs$1);
|
|
208
227
|
} else {
|
|
209
228
|
await this.db.disapproveDappUserCommitment(
|
|
210
229
|
commitmentId,
|
|
211
|
-
"CAPTCHA.INVALID_SOLUTION"
|
|
230
|
+
"CAPTCHA.INVALID_SOLUTION",
|
|
231
|
+
pairs$1
|
|
212
232
|
);
|
|
213
233
|
response = {
|
|
214
234
|
captchas: captchaIds.map((id) => ({
|
|
@@ -314,7 +334,7 @@ class ImgCaptchaManager extends captchaManager.CaptchaManager {
|
|
|
314
334
|
}
|
|
315
335
|
return void 0;
|
|
316
336
|
}
|
|
317
|
-
async verifyImageCaptchaSolution(user, dapp, commitmentId, maxVerifiedTime, ip) {
|
|
337
|
+
async verifyImageCaptchaSolution(user, dapp, commitmentId, env, maxVerifiedTime, ip) {
|
|
318
338
|
const solution = await (commitmentId ? this.getDappUserCommitmentById(commitmentId) : this.getDappUserCommitmentByAccount(user, dapp));
|
|
319
339
|
if (!solution) {
|
|
320
340
|
this.logger.debug(() => ({
|
|
@@ -322,10 +342,6 @@ class ImgCaptchaManager extends captchaManager.CaptchaManager {
|
|
|
322
342
|
}));
|
|
323
343
|
return { status: "API.USER_NOT_VERIFIED_NO_SOLUTION", verified: false };
|
|
324
344
|
}
|
|
325
|
-
const ipValidation = util.validateIpAddress(ip, solution.ipAddress, this.logger);
|
|
326
|
-
if (!ipValidation.isValid) {
|
|
327
|
-
return { status: "API.USER_NOT_VERIFIED", verified: false };
|
|
328
|
-
}
|
|
329
345
|
if (solution.serverChecked) {
|
|
330
346
|
return { status: "API.USER_ALREADY_VERIFIED", verified: false };
|
|
331
347
|
}
|
|
@@ -334,17 +350,43 @@ class ImgCaptchaManager extends captchaManager.CaptchaManager {
|
|
|
334
350
|
return { status: "API.USER_NOT_VERIFIED", verified: false };
|
|
335
351
|
}
|
|
336
352
|
maxVerifiedTime = maxVerifiedTime || 60 * 1e3;
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
353
|
+
const currentTime = Date.now();
|
|
354
|
+
const timeSinceCompletion = currentTime - solution.requestedAtTimestamp;
|
|
355
|
+
if (timeSinceCompletion > maxVerifiedTime) {
|
|
356
|
+
this.logger.debug(() => ({
|
|
357
|
+
msg: "Not verified - timed out"
|
|
358
|
+
}));
|
|
359
|
+
return {
|
|
360
|
+
status: "API.USER_NOT_VERIFIED_TIME_EXPIRED",
|
|
361
|
+
verified: false
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
if (ip) {
|
|
365
|
+
const solutionIpAddress = compositeIpAddress.getIpAddressFromComposite(solution.ipAddress);
|
|
366
|
+
const clientRecord = await this.db.getClientRecord(dapp);
|
|
367
|
+
const ipValidationRules = clientRecord?.settings?.ipValidationRules;
|
|
368
|
+
await this.db.updateDappUserCommitment(solution.id, {
|
|
369
|
+
providedIp: compositeIpAddress.getCompositeIpAddress(ip)
|
|
370
|
+
});
|
|
371
|
+
const ipValidation = await util.deepValidateIpAddress(
|
|
372
|
+
ip,
|
|
373
|
+
solutionIpAddress,
|
|
374
|
+
this.logger,
|
|
375
|
+
env.config.ipApi.apiKey,
|
|
376
|
+
env.config.ipApi.baseUrl,
|
|
377
|
+
ipValidationRules
|
|
378
|
+
);
|
|
379
|
+
if (!ipValidation.isValid) {
|
|
380
|
+
this.logger.error(() => ({
|
|
381
|
+
msg: "IP validation failed for image captcha",
|
|
382
|
+
data: {
|
|
383
|
+
ip,
|
|
384
|
+
solutionIp: solutionIpAddress.address,
|
|
385
|
+
error: ipValidation.errorMessage,
|
|
386
|
+
distanceKm: ipValidation.distanceKm
|
|
387
|
+
}
|
|
343
388
|
}));
|
|
344
|
-
return {
|
|
345
|
-
status: "API.USER_NOT_VERIFIED_TIME_EXPIRED",
|
|
346
|
-
verified: false
|
|
347
|
-
};
|
|
389
|
+
return { status: "API.USER_NOT_VERIFIED", verified: false };
|
|
348
390
|
}
|
|
349
391
|
}
|
|
350
392
|
const isApproved = solution.result.status === types.CaptchaStatus.approved;
|
|
@@ -4,6 +4,7 @@ const util = require("@polkadot/util");
|
|
|
4
4
|
const common = require("@prosopo/common");
|
|
5
5
|
const types = require("@prosopo/types");
|
|
6
6
|
const util$1 = require("@prosopo/util");
|
|
7
|
+
const compositeIpAddress = require("../../compositeIpAddress.cjs");
|
|
7
8
|
const util$2 = require("../../util.cjs");
|
|
8
9
|
const captchaManager = require("../captchaManager.cjs");
|
|
9
10
|
const frictionlessTasksUtils = require("../frictionless/frictionlessTasksUtils.cjs");
|
|
@@ -47,7 +48,7 @@ class PowCaptchaManager extends captchaManager.CaptchaManager {
|
|
|
47
48
|
* @param ipAddress
|
|
48
49
|
* @param headers
|
|
49
50
|
*/
|
|
50
|
-
async verifyPowCaptchaSolution(challenge,
|
|
51
|
+
async verifyPowCaptchaSolution(challenge, providerChallengeSignature, nonce, timeout, userTimestampSignature, ipAddress, headers) {
|
|
51
52
|
powTasksUtils.checkPowSignature(
|
|
52
53
|
challenge,
|
|
53
54
|
providerChallengeSignature,
|
|
@@ -70,8 +71,9 @@ class PowCaptchaManager extends captchaManager.CaptchaManager {
|
|
|
70
71
|
}));
|
|
71
72
|
return false;
|
|
72
73
|
}
|
|
74
|
+
const difficulty = challengeRecord.difficulty;
|
|
73
75
|
if (!util$1.verifyRecency(challenge, timeout)) {
|
|
74
|
-
await this.db.
|
|
76
|
+
await this.db.updatePowCaptchaRecordResult(
|
|
75
77
|
challenge,
|
|
76
78
|
{
|
|
77
79
|
status: types.CaptchaStatus.disapproved,
|
|
@@ -93,7 +95,7 @@ class PowCaptchaManager extends captchaManager.CaptchaManager {
|
|
|
93
95
|
reason: "CAPTCHA.INVALID_SOLUTION"
|
|
94
96
|
};
|
|
95
97
|
}
|
|
96
|
-
await this.db.
|
|
98
|
+
await this.db.updatePowCaptchaRecordResult(
|
|
97
99
|
challenge,
|
|
98
100
|
result,
|
|
99
101
|
false,
|
|
@@ -111,21 +113,14 @@ class PowCaptchaManager extends captchaManager.CaptchaManager {
|
|
|
111
113
|
* @param {number} timeout - the time in milliseconds since the Provider was selected to provide the PoW captcha
|
|
112
114
|
* @param ip
|
|
113
115
|
*/
|
|
114
|
-
async serverVerifyPowCaptchaSolution(dappAccount, challenge, timeout, ip) {
|
|
116
|
+
async serverVerifyPowCaptchaSolution(dappAccount, challenge, timeout, env, ip) {
|
|
117
|
+
const notVerifiedResponse = { verified: false };
|
|
115
118
|
const challengeRecord = await this.db.getPowCaptchaRecordByChallenge(challenge);
|
|
116
119
|
if (!challengeRecord) {
|
|
117
120
|
this.logger.debug(() => ({
|
|
118
121
|
msg: `No record of this challenge: ${challenge}`
|
|
119
122
|
}));
|
|
120
|
-
return
|
|
121
|
-
}
|
|
122
|
-
const ipValidation = util$2.validateIpAddress(
|
|
123
|
-
ip,
|
|
124
|
-
challengeRecord.ipAddress,
|
|
125
|
-
this.logger
|
|
126
|
-
);
|
|
127
|
-
if (!ipValidation.isValid) {
|
|
128
|
-
return { verified: false };
|
|
123
|
+
return notVerifiedResponse;
|
|
129
124
|
}
|
|
130
125
|
if (challengeRecord.result.status !== types.CaptchaStatus.approved) {
|
|
131
126
|
throw new common.ProsopoApiError("CAPTCHA.INVALID_SOLUTION", {
|
|
@@ -135,7 +130,7 @@ class PowCaptchaManager extends captchaManager.CaptchaManager {
|
|
|
135
130
|
}
|
|
136
131
|
});
|
|
137
132
|
}
|
|
138
|
-
if (challengeRecord.serverChecked) return
|
|
133
|
+
if (challengeRecord.serverChecked) return notVerifiedResponse;
|
|
139
134
|
const challengeDappAccount = challengeRecord.dappAccount;
|
|
140
135
|
if (dappAccount !== challengeDappAccount) {
|
|
141
136
|
throw new common.ProsopoEnvError("CAPTCHA.DAPP_USER_SOLUTION_NOT_FOUND", {
|
|
@@ -146,10 +141,43 @@ class PowCaptchaManager extends captchaManager.CaptchaManager {
|
|
|
146
141
|
}
|
|
147
142
|
});
|
|
148
143
|
}
|
|
149
|
-
util$1.verifyRecency(challenge, timeout);
|
|
150
144
|
await this.db.markDappUserPoWCommitmentsChecked([
|
|
151
145
|
challengeRecord.challenge
|
|
152
146
|
]);
|
|
147
|
+
const recent = util$1.verifyRecency(challenge, timeout);
|
|
148
|
+
if (!recent) {
|
|
149
|
+
return notVerifiedResponse;
|
|
150
|
+
}
|
|
151
|
+
if (ip) {
|
|
152
|
+
const challengeIpAddress = compositeIpAddress.getIpAddressFromComposite(
|
|
153
|
+
challengeRecord.ipAddress
|
|
154
|
+
);
|
|
155
|
+
const clientRecord = await this.db.getClientRecord(dappAccount);
|
|
156
|
+
const ipValidationRules = clientRecord?.settings?.ipValidationRules;
|
|
157
|
+
await this.db.updatePowCaptchaRecord(challengeRecord.challenge, {
|
|
158
|
+
providedIp: compositeIpAddress.getCompositeIpAddress(ip)
|
|
159
|
+
});
|
|
160
|
+
const ipValidation = await util$2.deepValidateIpAddress(
|
|
161
|
+
ip,
|
|
162
|
+
challengeIpAddress,
|
|
163
|
+
this.logger,
|
|
164
|
+
env.config.ipApi.apiKey,
|
|
165
|
+
env.config.ipApi.baseUrl,
|
|
166
|
+
ipValidationRules
|
|
167
|
+
);
|
|
168
|
+
if (!ipValidation.isValid) {
|
|
169
|
+
this.logger.error(() => ({
|
|
170
|
+
msg: "IP validation failed for PoW captcha",
|
|
171
|
+
data: {
|
|
172
|
+
ip,
|
|
173
|
+
challengeIp: challengeIpAddress.address,
|
|
174
|
+
error: ipValidation.errorMessage,
|
|
175
|
+
distanceKm: ipValidation.distanceKm
|
|
176
|
+
}
|
|
177
|
+
}));
|
|
178
|
+
return notVerifiedResponse;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
153
181
|
let score;
|
|
154
182
|
if (challengeRecord.frictionlessTokenId) {
|
|
155
183
|
const tokenRecord = await this.db.getFrictionlessTokenRecordByTokenId(
|