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