@duckity/js 0.2.0 → 1.0.0
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/dist/index.d.ts +28 -2
- package/dist/index.js +1 -1
- package/dist/worker.js +1 -1
- package/package.json +5 -1
- package/src/index.ts +71 -15
- package/src/processing/index.ts +66 -44
- package/src/processing/wrapper.ts +23 -11
- package/src/requests.ts +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -17,9 +17,35 @@ interface GetDuckityChallengeOptions {
|
|
|
17
17
|
*
|
|
18
18
|
* @param protectionProfileId The ID of the protection profile to get the challenge for.
|
|
19
19
|
* @param options Optional parameters for the challenge issuance.
|
|
20
|
+
*
|
|
20
21
|
* @returns The solution to the challenge issued by Duckity.
|
|
21
22
|
*/
|
|
22
23
|
declare function solve(protectionProfileId: string, options?: GetDuckityChallengeOptions): Promise<string>;
|
|
24
|
+
/**
|
|
25
|
+
* Options for validating a Duckity solution with the API.
|
|
26
|
+
*/
|
|
27
|
+
interface ValidateDuckitySolutionOptions {
|
|
28
|
+
/**
|
|
29
|
+
* The base URL to the API endpoint. Defaults to `https://api.duckity.com/d1` if not provided.
|
|
30
|
+
*
|
|
31
|
+
* The version must be specified in the URL path.
|
|
32
|
+
*
|
|
33
|
+
* Only update this when self-hosting a Duckling.
|
|
34
|
+
*/
|
|
35
|
+
api?: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Validates a client-submitted solution token.
|
|
39
|
+
*
|
|
40
|
+
* @param solution The solution token to validate.
|
|
41
|
+
* @param clientIp The IP of the client that submitted the solution token.
|
|
42
|
+
* @param applicationSecret The protection profile's application secret.
|
|
43
|
+
* @param protectionProfileId The ID of the protection profile for which this challenge was issued.
|
|
44
|
+
* @param options Additional options to customize the behavior of this call.
|
|
45
|
+
*
|
|
46
|
+
* @returns Whether the solution was valid or not.
|
|
47
|
+
*/
|
|
48
|
+
declare function validate(solution: string, clientIp: string, applicationSecret: string, protectionProfileId: string, options?: ValidateDuckitySolutionOptions): Promise<boolean>;
|
|
23
49
|
|
|
24
|
-
export { solve };
|
|
25
|
-
export type { GetDuckityChallengeOptions };
|
|
50
|
+
export { solve, validate };
|
|
51
|
+
export type { GetDuckityChallengeOptions, ValidateDuckitySolutionOptions };
|
package/dist/index.js
CHANGED
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
* Copyright 2019 Google LLC
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
|
-
const e=Symbol("Comlink.proxy"),t=Symbol("Comlink.endpoint"),n=Symbol("Comlink.releaseProxy"),r=Symbol("Comlink.finalizer"),a=Symbol("Comlink.thrown"),i=e=>"object"==typeof e&&null!==e||"function"==typeof e,
|
|
6
|
+
const e=Symbol("Comlink.proxy"),t=Symbol("Comlink.endpoint"),n=Symbol("Comlink.releaseProxy"),r=Symbol("Comlink.finalizer"),a=Symbol("Comlink.thrown"),i=e=>"object"==typeof e&&null!==e||"function"==typeof e,o=new Map([["proxy",{canHandle:t=>i(t)&&t[e],serialize(e){const{port1:t,port2:n}=new MessageChannel;return s(e,t),[n,[n]]},deserialize:e=>(e.start(),u(e))}],["throw",{canHandle:e=>i(e)&&a in e,serialize({value:e}){let t;return t=e instanceof Error?{isError:!0,value:{message:e.message,name:e.name,stack:e.stack}}:{isError:!1,value:e},[t,[]]},deserialize(e){if(e.isError)throw Object.assign(new Error(e.value.message),e.value);throw e.value}}]]);function s(t,n=globalThis,i=["*"]){n.addEventListener("message",function o(u){if(!u||!u.data)return;if(!function(e,t){for(const n of e){if(t===n||"*"===n)return!0;if(n instanceof RegExp&&n.test(t))return!0}return!1}(i,u.origin))return void console.warn(`Invalid origin '${u.origin}' for comlink proxy`);const{id:l,type:p,path:f}=Object.assign({path:[]},u.data),d=(u.data.argumentList||[]).map(w);let h;try{const n=f.slice(0,-1).reduce((e,t)=>e[t],t),r=f.reduce((e,t)=>e[t],t);switch(p){case"GET":h=r;break;case"SET":n[f.slice(-1)[0]]=w(u.data.value),h=!0;break;case"APPLY":h=r.apply(n,d);break;case"CONSTRUCT":h=function(t){return Object.assign(t,{[e]:!0})}(new r(...d));break;case"ENDPOINT":{const{port1:e,port2:n}=new MessageChannel;s(t,n),h=function(e,t){return g.set(e,t),e}(e,[e])}break;case"RELEASE":h=void 0;break;default:return}}catch(e){h={value:e,[a]:0}}Promise.resolve(h).catch(e=>({value:e,[a]:0})).then(e=>{const[a,i]=m(e);n.postMessage(Object.assign(Object.assign({},a),{id:l}),i),"RELEASE"===p&&(n.removeEventListener("message",o),c(n),r in t&&"function"==typeof t[r]&&t[r]())}).catch(e=>{const[t,r]=m({value:new TypeError("Unserializable return value"),[a]:0});n.postMessage(Object.assign(Object.assign({},t),{id:l}),r)})}),n.start&&n.start()}function c(e){(function(e){return"MessagePort"===e.constructor.name})(e)&&e.close()}function u(e,t){const n=new Map;return e.addEventListener("message",function(e){const{data:t}=e;if(!t||!t.id)return;const r=n.get(t.id);if(r)try{r(t)}finally{n.delete(t.id)}}),h(e,n,[],t)}function l(e){if(e)throw new Error("Proxy has been released and is not useable")}function p(e){return b(e,new Map,{type:"RELEASE"}).then(()=>{c(e)})}const f=new WeakMap,d="FinalizationRegistry"in globalThis&&new FinalizationRegistry(e=>{const t=(f.get(e)||0)-1;f.set(e,t),0===t&&p(e)});function h(e,r,a=[],i=function(){}){let o=!1;const s=new Proxy(i,{get(t,i){if(l(o),i===n)return()=>{!function(e){d&&d.unregister(e)}(s),p(e),r.clear(),o=!0};if("then"===i){if(0===a.length)return{then:()=>s};const t=b(e,r,{type:"GET",path:a.map(e=>e.toString())}).then(w);return t.then.bind(t)}return h(e,r,[...a,i])},set(t,n,i){l(o);const[s,c]=m(i);return b(e,r,{type:"SET",path:[...a,n].map(e=>e.toString()),value:s},c).then(w)},apply(n,i,s){l(o);const c=a[a.length-1];if(c===t)return b(e,r,{type:"ENDPOINT"}).then(w);if("bind"===c)return h(e,r,a.slice(0,-1));const[u,p]=y(s);return b(e,r,{type:"APPLY",path:a.map(e=>e.toString()),argumentList:u},p).then(w)},construct(t,n){l(o);const[i,s]=y(n);return b(e,r,{type:"CONSTRUCT",path:a.map(e=>e.toString()),argumentList:i},s).then(w)}});return function(e,t){const n=(f.get(t)||0)+1;f.set(t,n),d&&d.register(e,t,e)}(s,e),s}function y(e){const t=e.map(m);return[t.map(e=>e[0]),(n=t.map(e=>e[1]),Array.prototype.concat.apply([],n))];var n}const g=new WeakMap;function m(e){for(const[t,n]of o)if(n.canHandle(e)){const[r,a]=n.serialize(e);return[{type:"HANDLER",name:t,value:r},a]}return[{type:"RAW",value:e},g.get(e)||[]]}function w(e){switch(e.type){case"HANDLER":return o.get(e.name).deserialize(e.value);case"RAW":return e.value}}function b(e,t,n,r){return new Promise(a=>{const i=new Array(4).fill(0).map(()=>Math.floor(Math.random()*Number.MAX_SAFE_INTEGER).toString(16)).join("-");t.set(i,a),e.start&&e.start(),e.postMessage(Object.assign({id:i},n),r)})}let E,v;function k(e,t){return async function(e,t){const n=await fetch(e,t);let r;try{r=await n.json()}catch{r=await n.text()}if(!n.ok)throw new Error(r?.message||"Request failed");return r}(e,{method:"POST",headers:{...t?.body?{"Content-Type":"application/json"}:{},...t?.headers},...t,body:t?.body?JSON.stringify(t.body):void 0})}async function S(e,t){t?.api&&t.api.endsWith("/")&&(t.api=t.api.slice(0,-1));let n=await k(`${t?.api||"https://api.duckity.com/d1"}/challenges/${e}/issue`,{headers:{"x-duckity-csrf":"1"}});return await function(){if("undefined"==typeof window)throw new Error("Worker API can only be used in the browser");return v||(E=new Worker(new URL("./worker.js",import.meta.url),{type:"module"}),v=u(E)),v}().solve(n.challenge)}async function T(e,t,n,r,a){return a?.api&&a.api.endsWith("/")&&(a.api=a.api.slice(0,-1)),(await k(`${a?.api||"https://api.duckity.com/d1"}/challenges/${r}/validate`,{headers:{Authorization:`Bearer ${n}`},body:{solution:e,ip:t}})).is_valid}export{S as solve,T as validate};
|
package/dist/worker.js
CHANGED
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
* Copyright 2019 Google LLC
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
|
-
const n=Symbol("Comlink.proxy"),e=Symbol("Comlink.endpoint"),t=Symbol("Comlink.releaseProxy"),r=Symbol("Comlink.finalizer"),
|
|
6
|
+
const n=Symbol("Comlink.proxy"),e=Symbol("Comlink.endpoint"),t=Symbol("Comlink.releaseProxy"),r=Symbol("Comlink.finalizer"),a=Symbol("Comlink.thrown"),o=n=>"object"==typeof n&&null!==n||"function"==typeof n,i={canHandle:e=>o(e)&&e[n],serialize(n){const{port1:e,port2:t}=new MessageChannel;return c(n,e),[t,[t]]},deserialize:n=>(n.start(),function(n,e){const t=new Map;return n.addEventListener("message",function(n){const{data:e}=n;if(!e||!e.id)return;const r=t.get(e.id);if(r)try{r(e)}finally{t.delete(e.id)}}),d(n,t,[],e)}(n))},s=new Map([["proxy",i],["throw",{canHandle:n=>o(n)&&a in n,serialize({value:n}){let e;return e=n instanceof Error?{isError:!0,value:{message:n.message,name:n.name,stack:n.stack}}:{isError:!1,value:n},[e,[]]},deserialize(n){if(n.isError)throw Object.assign(new Error(n.value.message),n.value);throw n.value}}]]);function c(e,t=globalThis,o=["*"]){t.addEventListener("message",function i(s){if(!s||!s.data)return;if(!function(n,e){for(const t of n){if(e===t||"*"===t)return!0;if(t instanceof RegExp&&t.test(e))return!0}return!1}(o,s.origin))return void console.warn(`Invalid origin '${s.origin}' for comlink proxy`);const{id:u,type:f,path:p}=Object.assign({path:[]},s.data),g=(s.data.argumentList||[]).map(b);let d;try{const t=p.slice(0,-1).reduce((n,e)=>n[e],e),r=p.reduce((n,e)=>n[e],e);switch(f){case"GET":d=r;break;case"SET":t[p.slice(-1)[0]]=b(s.data.value),d=!0;break;case"APPLY":d=r.apply(t,g);break;case"CONSTRUCT":d=function(e){return Object.assign(e,{[n]:!0})}(new r(...g));break;case"ENDPOINT":{const{port1:n,port2:t}=new MessageChannel;c(e,t),d=function(n,e){return m.set(n,e),n}(n,[n])}break;case"RELEASE":d=void 0;break;default:return}}catch(n){d={value:n,[a]:0}}Promise.resolve(d).catch(n=>({value:n,[a]:0})).then(n=>{const[a,o]=h(n);t.postMessage(Object.assign(Object.assign({},a),{id:u}),o),"RELEASE"===f&&(t.removeEventListener("message",i),l(t),r in e&&"function"==typeof e[r]&&e[r]())}).catch(n=>{const[e,r]=h({value:new TypeError("Unserializable return value"),[a]:0});t.postMessage(Object.assign(Object.assign({},e),{id:u}),r)})}),t.start&&t.start()}function l(n){(function(n){return"MessagePort"===n.constructor.name})(n)&&n.close()}function u(n){if(n)throw new Error("Proxy has been released and is not useable")}function f(n){return E(n,new Map,{type:"RELEASE"}).then(()=>{l(n)})}const p=new WeakMap,g="FinalizationRegistry"in globalThis&&new FinalizationRegistry(n=>{const e=(p.get(n)||0)-1;p.set(n,e),0===e&&f(n)});function d(n,r,a=[],o=function(){}){let i=!1;const s=new Proxy(o,{get(e,o){if(u(i),o===t)return()=>{!function(n){g&&g.unregister(n)}(s),f(n),r.clear(),i=!0};if("then"===o){if(0===a.length)return{then:()=>s};const e=E(n,r,{type:"GET",path:a.map(n=>n.toString())}).then(b);return e.then.bind(e)}return d(n,r,[...a,o])},set(e,t,o){u(i);const[s,c]=h(o);return E(n,r,{type:"SET",path:[...a,t].map(n=>n.toString()),value:s},c).then(b)},apply(t,o,s){u(i);const c=a[a.length-1];if(c===e)return E(n,r,{type:"ENDPOINT"}).then(b);if("bind"===c)return d(n,r,a.slice(0,-1));const[l,f]=y(s);return E(n,r,{type:"APPLY",path:a.map(n=>n.toString()),argumentList:l},f).then(b)},construct(e,t){u(i);const[o,s]=y(t);return E(n,r,{type:"CONSTRUCT",path:a.map(n=>n.toString()),argumentList:o},s).then(b)}});return function(n,e){const t=(p.get(e)||0)+1;p.set(e,t),g&&g.register(n,e,n)}(s,n),s}function y(n){const e=n.map(h);return[e.map(n=>n[0]),(t=e.map(n=>n[1]),Array.prototype.concat.apply([],t))];var t}const m=new WeakMap;function h(n){for(const[e,t]of s)if(t.canHandle(n)){const[r,a]=t.serialize(n);return[{type:"HANDLER",name:e,value:r},a]}return[{type:"RAW",value:n},m.get(n)||[]]}function b(n){switch(n.type){case"HANDLER":return s.get(n.name).deserialize(n.value);case"RAW":return n.value}}function E(n,e,t,r){return new Promise(a=>{const o=new Array(4).fill(0).map(()=>Math.floor(Math.random()*Number.MAX_SAFE_INTEGER).toString(16)).join("-");e.set(o,a),n.start&&n.start(),n.postMessage(Object.assign({id:o},t),r)})}const w=512;function v(n,e){let t=0n;for(const r of n)t=t<<e|BigInt(r);return t}function A(n,e,t){void 0===t&&(t=32n);let r=e/Number(t/8n),a=new Array(r).fill(0),o=0n;for(let n=t;n>0;n--)o<<=1n,o|=1n;for(let e=r-1;e>=0;e--){let r=Number(n&o);a[e]=r,n>>=t}return a}function S(n,e,t){let r=1n;for(;0n!==e;)1n==(1n&e)&&(r*=n,r%=t),n**=2n,n%=t,e/=2n;return r}function k(n,e){if([1n,2n,3n,5n,7n].includes(n))return!0;if(0n==(1n&n))return!1;if(n%5n==0n)return!1;for(let t=0n;t<BigInt(e)&&2n+t<n;t++){if(!T(n,2n+t))return!1}return!0}function T(n,e){let t=n-1n,r=t,a=0;for(;r%2n==0n;)r/=2n,a+=1;let o=S(e,r,n);for(let e=0n;e<a;e++){if(1n==o||o==t)return!0;o=S(o,2n,n)}return!1}var M={encode:function(n,e){let t={y:A(e.y,512,32n),pi:A(e.pi,512,32n)},r=JSON.stringify(t);return`${n}.${btoa(r).replaceAll("=","").replaceAll("+","-").replaceAll("/","_")}`},solve:async function(n){let e=n.x;for(let t=0;t<n.t;t++)e=S(e,2n,n.n);let t=Array.from((new TextEncoder).encode("duckity"));t=t.concat(...A(n.n,w,8n)),t=t.concat(...A(n.x,w,8n)),t=t.concat(...A(BigInt(n.t),w,8n)),t=t.concat(...A(e,w,8n));let r=await crypto.subtle.digest("SHA-256",new Uint8Array(t)),a=function(n){for(;;)if(k(n+=1n,40))return n}(v(Array.from(new Uint8Array(r)),8n)),o=1n,i=1n;for(let e=0n;e<n.t;e++)o*=2n,o>=a?(o-=a,i=i*i*n.x%n.n):i=i*i%n.n;return{y:e,pi:i}},decode:function(n){if(![1,2].includes((n.match(/\./g)||[]).length))throw Error("The challenge string contained too many or not enough sections.");let[e,t]=n.split(".",2),r=atob(e),a=JSON.parse(r);return{id:a.challenge_id,ip:a.ip,n:v(a.n,32n),x:v(a.x,32n),t:a.t,timestamp:new Date(a.timestamp)}}};c({async solve(n){let e=M.decode(n),t=await M.solve(e);return M.encode(n,t)}});
|
package/package.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@duckity/js",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "1.0.0",
|
|
5
5
|
"module": "src/index.ts",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
|
+
"homepage": "https://duckity.com/docs/sdks/javascript",
|
|
8
|
+
"repository": {
|
|
9
|
+
"url": "https://github.com/duckity-com/sdks"
|
|
10
|
+
},
|
|
7
11
|
"collaborators": [
|
|
8
12
|
"Rafael Bradley <rafabradleyrb@gmail.com>"
|
|
9
13
|
],
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { getWorkerApi } from "./processing/wrapper";
|
|
2
2
|
import { post } from "./requests";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -13,19 +13,6 @@ export interface GetDuckityChallengeOptions {
|
|
|
13
13
|
* Only update this when self-hosting a Duckling.
|
|
14
14
|
*/
|
|
15
15
|
api?: string;
|
|
16
|
-
|
|
17
|
-
// /**
|
|
18
|
-
// * When this is `false`, the client will assume it is allowed to get a challenge the first time
|
|
19
|
-
// * one is requested. This may not always be true (when the client has an active penalty or used
|
|
20
|
-
// * all of their allowed requests in another session), case in which the client may be penalized.
|
|
21
|
-
// *
|
|
22
|
-
// * When this is `true`, an extra request will be made to the Duckling API to synchronize the
|
|
23
|
-
// * client's state with the server's state. This extra request will not issue a challenge, rather
|
|
24
|
-
// * only synchronize the client's state with the server's state. If the client is penalized or has
|
|
25
|
-
// * to wait before being allowed to get a challenge, the client will be informed of this. It'll
|
|
26
|
-
// * avoid getting it unnecessarily penalized at the cost of an extra request.
|
|
27
|
-
// */
|
|
28
|
-
// sync?: boolean;
|
|
29
16
|
}
|
|
30
17
|
|
|
31
18
|
/**
|
|
@@ -44,6 +31,7 @@ interface ChallengeResponse {
|
|
|
44
31
|
*
|
|
45
32
|
* @param protectionProfileId The ID of the protection profile to get the challenge for.
|
|
46
33
|
* @param options Optional parameters for the challenge issuance.
|
|
34
|
+
*
|
|
47
35
|
* @returns The solution to the challenge issued by Duckity.
|
|
48
36
|
*/
|
|
49
37
|
export async function solve(
|
|
@@ -57,9 +45,77 @@ export async function solve(
|
|
|
57
45
|
|
|
58
46
|
let response: ChallengeResponse = await post(
|
|
59
47
|
`${options?.api || "https://api.duckity.com/d1"}/challenges/${protectionProfileId}/issue`,
|
|
48
|
+
{
|
|
49
|
+
headers: {
|
|
50
|
+
"x-duckity-csrf": "1",
|
|
51
|
+
}
|
|
52
|
+
}
|
|
60
53
|
);
|
|
61
54
|
|
|
62
|
-
let solution = await
|
|
55
|
+
let solution = await getWorkerApi().solve(response.challenge);
|
|
63
56
|
|
|
64
57
|
return solution;
|
|
65
58
|
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Options for validating a Duckity solution with the API.
|
|
62
|
+
*/
|
|
63
|
+
export interface ValidateDuckitySolutionOptions {
|
|
64
|
+
/**
|
|
65
|
+
* The base URL to the API endpoint. Defaults to `https://api.duckity.com/d1` if not provided.
|
|
66
|
+
*
|
|
67
|
+
* The version must be specified in the URL path.
|
|
68
|
+
*
|
|
69
|
+
* Only update this when self-hosting a Duckling.
|
|
70
|
+
*/
|
|
71
|
+
api?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The response from the Duckling API when validating a solution.
|
|
76
|
+
*/
|
|
77
|
+
interface ValidationResponse {
|
|
78
|
+
/**
|
|
79
|
+
* Whether the submitted solution is valid.
|
|
80
|
+
*/
|
|
81
|
+
is_valid: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Validates a client-submitted solution token.
|
|
86
|
+
*
|
|
87
|
+
* @param solution The solution token to validate.
|
|
88
|
+
* @param clientIp The IP of the client that submitted the solution token.
|
|
89
|
+
* @param applicationSecret The protection profile's application secret.
|
|
90
|
+
* @param protectionProfileId The ID of the protection profile for which this challenge was issued.
|
|
91
|
+
* @param options Additional options to customize the behavior of this call.
|
|
92
|
+
*
|
|
93
|
+
* @returns Whether the solution was valid or not.
|
|
94
|
+
*/
|
|
95
|
+
export async function validate(
|
|
96
|
+
solution: string,
|
|
97
|
+
clientIp: string,
|
|
98
|
+
applicationSecret: string,
|
|
99
|
+
protectionProfileId: string,
|
|
100
|
+
options?: ValidateDuckitySolutionOptions,
|
|
101
|
+
): Promise<boolean> {
|
|
102
|
+
if (options?.api && options.api.endsWith("/")) {
|
|
103
|
+
// Remove the trailing slash from the API URL if provided
|
|
104
|
+
options.api = options.api.slice(0, -1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let response: ValidationResponse = await post(
|
|
108
|
+
`${options?.api || "https://api.duckity.com/d1"}/challenges/${protectionProfileId}/validate`,
|
|
109
|
+
{
|
|
110
|
+
headers: {
|
|
111
|
+
Authorization: `Bearer ${applicationSecret}`,
|
|
112
|
+
},
|
|
113
|
+
body: {
|
|
114
|
+
solution,
|
|
115
|
+
ip: clientIp,
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
return response.is_valid;
|
|
121
|
+
}
|
package/src/processing/index.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const RSA_MODULUS_SIZE = 512;
|
|
2
|
+
|
|
1
3
|
interface ChallengeMeta {
|
|
2
4
|
// The challenge's ID.
|
|
3
5
|
challenge_id: string;
|
|
@@ -8,9 +10,9 @@ interface ChallengeMeta {
|
|
|
8
10
|
// The ID of the protection profile this challenge was issued for.
|
|
9
11
|
protection_profile_id: string;
|
|
10
12
|
// The digits of the N Wesolowski VDF parameter, most significant digit first.
|
|
11
|
-
n:
|
|
13
|
+
n: number[];
|
|
12
14
|
// The digits of the X Wesolowski VDF parameter, most significant digit first.
|
|
13
|
-
x:
|
|
15
|
+
x: number[];
|
|
14
16
|
// The T Wesolowski VDF parameter.
|
|
15
17
|
t: number;
|
|
16
18
|
}
|
|
@@ -64,7 +66,7 @@ function getBigintWidth(number: bigint, alignment?: number): number {
|
|
|
64
66
|
bitWidth = number.toString(2).length;
|
|
65
67
|
}
|
|
66
68
|
|
|
67
|
-
let byteWidth = Math.
|
|
69
|
+
let byteWidth = Math.floor((bitWidth + 7) / 8);
|
|
68
70
|
|
|
69
71
|
if (alignment !== undefined) {
|
|
70
72
|
byteWidth = Math.ceil(byteWidth / alignment) * alignment;
|
|
@@ -73,33 +75,41 @@ function getBigintWidth(number: bigint, alignment?: number): number {
|
|
|
73
75
|
return byteWidth;
|
|
74
76
|
}
|
|
75
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Returns a digit array from a big integer.
|
|
80
|
+
*
|
|
81
|
+
* @param number The number to get the digits from.
|
|
82
|
+
* @param width The byte width of the result. If undefined, this will be the minimum width required
|
|
83
|
+
* to represent the full number.
|
|
84
|
+
* @param digitBits The amount of bits per digit.
|
|
85
|
+
* @returns The digits, MSBF.
|
|
86
|
+
*/
|
|
76
87
|
function getDigitsFromBigint(
|
|
77
88
|
number: bigint,
|
|
78
89
|
width?: number,
|
|
79
90
|
digitBits?: bigint,
|
|
80
91
|
): number[] {
|
|
81
|
-
let bytes: number[] = new Array(width).fill(0);
|
|
82
|
-
|
|
83
92
|
if (digitBits === undefined) {
|
|
84
93
|
digitBits = 32n;
|
|
85
94
|
}
|
|
86
95
|
|
|
87
96
|
if (width === undefined) {
|
|
88
|
-
width =
|
|
89
|
-
(BigInt(number.toString(2).length) / digitBits).toString(),
|
|
90
|
-
);
|
|
97
|
+
width = getBigintWidth(number, 1);
|
|
91
98
|
}
|
|
92
99
|
|
|
100
|
+
let digits = width / Number(digitBits / 8n);
|
|
101
|
+
let bytes: number[] = new Array(digits).fill(0);
|
|
102
|
+
|
|
93
103
|
let mask = 0n;
|
|
94
|
-
for (let i = digitBits
|
|
104
|
+
for (let i = digitBits; i > 0; i--) {
|
|
95
105
|
mask <<= 1n;
|
|
96
|
-
mask
|
|
106
|
+
mask |= 1n;
|
|
97
107
|
}
|
|
98
108
|
|
|
99
|
-
for (let i =
|
|
100
|
-
let byte =
|
|
101
|
-
number = number >> digitBits;
|
|
109
|
+
for (let i = digits - 1; i >= 0; i--) {
|
|
110
|
+
let byte = Number(number & mask);
|
|
102
111
|
bytes[i] = byte;
|
|
112
|
+
number = number >> digitBits;
|
|
103
113
|
}
|
|
104
114
|
|
|
105
115
|
return bytes;
|
|
@@ -153,24 +163,24 @@ function isPrime(number: bigint, tests: number): boolean {
|
|
|
153
163
|
}
|
|
154
164
|
|
|
155
165
|
function isPrimeForBase(number: bigint, base: bigint): boolean {
|
|
156
|
-
let
|
|
166
|
+
let numberMinusOne = number - 1n;
|
|
157
167
|
|
|
158
|
-
let odd =
|
|
159
|
-
let
|
|
168
|
+
let odd = numberMinusOne;
|
|
169
|
+
let baseTimes = 0;
|
|
160
170
|
|
|
161
171
|
while (odd % 2n == 0n) {
|
|
162
172
|
odd /= 2n;
|
|
163
|
-
|
|
173
|
+
baseTimes += 1;
|
|
164
174
|
}
|
|
165
175
|
|
|
166
|
-
let
|
|
176
|
+
let oddPower = modPow(base, odd, number);
|
|
167
177
|
|
|
168
|
-
for (let i = 0n; i <
|
|
169
|
-
if (
|
|
178
|
+
for (let i = 0n; i < baseTimes; i++) {
|
|
179
|
+
if (oddPower == 1n || oddPower == numberMinusOne) {
|
|
170
180
|
return true;
|
|
171
181
|
}
|
|
172
182
|
|
|
173
|
-
|
|
183
|
+
oddPower = modPow(oddPower, 2n, number);
|
|
174
184
|
}
|
|
175
185
|
|
|
176
186
|
return false;
|
|
@@ -186,9 +196,15 @@ function getNextPrime(number: bigint): bigint {
|
|
|
186
196
|
}
|
|
187
197
|
}
|
|
188
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Extracts the challenge's data from the string.
|
|
201
|
+
*
|
|
202
|
+
* @param challenge The raw challenge string.
|
|
203
|
+
* @returns The decoded challenge metadata.
|
|
204
|
+
*/
|
|
189
205
|
export function decode(challenge: string): Challenge {
|
|
190
|
-
// Counts all dots in the challenge string
|
|
191
|
-
if ((challenge.match(/\./g) || []).length
|
|
206
|
+
// Counts all dots in the challenge string.
|
|
207
|
+
if (![1, 2].includes((challenge.match(/\./g) || []).length)) {
|
|
192
208
|
throw Error(
|
|
193
209
|
"The challenge string contained too many or not enough sections.",
|
|
194
210
|
);
|
|
@@ -222,38 +238,41 @@ export function decode(challenge: string): Challenge {
|
|
|
222
238
|
*/
|
|
223
239
|
export async function solve(challenge: Challenge): Promise<Solution> {
|
|
224
240
|
let y = challenge.x;
|
|
225
|
-
|
|
226
241
|
for (let i = 0; i < challenge.t; i++) {
|
|
227
|
-
y = y
|
|
242
|
+
y = modPow(y, 2n, challenge.n);
|
|
228
243
|
}
|
|
229
244
|
|
|
230
|
-
// Align to 4 bytes since each digit is a u32.
|
|
231
|
-
let width = getBigintWidth(y, 4);
|
|
232
|
-
|
|
233
|
-
let xBytes = getDigitsFromBigint(challenge.x, width, 8n);
|
|
234
|
-
let yBytes = getDigitsFromBigint(y, width, 8n);
|
|
235
|
-
|
|
236
245
|
let bytes = Array.from(new TextEncoder().encode("duckity"));
|
|
237
|
-
bytes.concat(
|
|
238
|
-
|
|
246
|
+
bytes = bytes.concat(
|
|
247
|
+
...getDigitsFromBigint(challenge.n, RSA_MODULUS_SIZE, 8n),
|
|
248
|
+
);
|
|
249
|
+
bytes = bytes.concat(
|
|
250
|
+
...getDigitsFromBigint(challenge.x, RSA_MODULUS_SIZE, 8n),
|
|
251
|
+
);
|
|
252
|
+
bytes = bytes.concat(
|
|
253
|
+
...getDigitsFromBigint(BigInt(challenge.t), RSA_MODULUS_SIZE, 8n),
|
|
254
|
+
);
|
|
255
|
+
bytes = bytes.concat(...getDigitsFromBigint(y, RSA_MODULUS_SIZE, 8n));
|
|
239
256
|
let hash = await crypto.subtle.digest("SHA-256", new Uint8Array(bytes));
|
|
240
257
|
let hashBytes = Array.from(new Uint8Array(hash));
|
|
241
258
|
|
|
242
259
|
let z = getBigintFromDigits(hashBytes, 8n);
|
|
243
260
|
let l = getNextPrime(z);
|
|
244
261
|
|
|
262
|
+
// We need 2^T = Q * L + R but 2^T is too big to calculate. Since 2^T is 2 << T in binary, we can
|
|
263
|
+
// calculate PI on the go by keeping that in mind without actually storing the whole number in
|
|
264
|
+
// memory.
|
|
265
|
+
let r = 1n;
|
|
245
266
|
let pi = 1n;
|
|
246
|
-
let acc = challenge.x;
|
|
247
|
-
let exp_mod_l = 1n;
|
|
248
267
|
|
|
249
|
-
for (let i =
|
|
250
|
-
|
|
268
|
+
for (let i = 0n; i < challenge.t; i++) {
|
|
269
|
+
r = r * 2n;
|
|
251
270
|
|
|
252
|
-
if (
|
|
253
|
-
|
|
254
|
-
|
|
271
|
+
if (r >= l) {
|
|
272
|
+
r = r - l;
|
|
273
|
+
pi = (pi * pi * challenge.x) % challenge.n;
|
|
255
274
|
} else {
|
|
256
|
-
|
|
275
|
+
pi = (pi * pi) % challenge.n;
|
|
257
276
|
}
|
|
258
277
|
}
|
|
259
278
|
|
|
@@ -265,12 +284,15 @@ export async function solve(challenge: Challenge): Promise<Solution> {
|
|
|
265
284
|
|
|
266
285
|
export function encode(original: string, solution: Solution): string {
|
|
267
286
|
let solutionMeta: SolutionMeta = {
|
|
268
|
-
y: getDigitsFromBigint(solution.y,
|
|
269
|
-
pi: getDigitsFromBigint(solution.pi,
|
|
287
|
+
y: getDigitsFromBigint(solution.y, 512, 32n),
|
|
288
|
+
pi: getDigitsFromBigint(solution.pi, 512, 32n),
|
|
270
289
|
};
|
|
271
290
|
|
|
272
291
|
let solutionMetaJson = JSON.stringify(solutionMeta);
|
|
273
|
-
let solutionMetaBase64 = btoa(solutionMetaJson)
|
|
292
|
+
let solutionMetaBase64 = btoa(solutionMetaJson)
|
|
293
|
+
.replaceAll("=", "")
|
|
294
|
+
.replaceAll("+", "-")
|
|
295
|
+
.replaceAll("/", "_");
|
|
274
296
|
|
|
275
297
|
return `${original}.${solutionMetaBase64}`;
|
|
276
298
|
}
|
|
@@ -1,17 +1,29 @@
|
|
|
1
1
|
import * as Comlink from "comlink";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
type WorkerApi = {
|
|
4
|
+
solve(challenge: string): Promise<string>;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
let worker: Worker | undefined;
|
|
8
|
+
let api: Comlink.Remote<WorkerApi> | undefined;
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
12
|
-
* worker thread. The
|
|
11
|
+
* Wraps the worker with Comlink to allow for easy communication between the main thread and the
|
|
12
|
+
* worker thread. The API exposes a `solve` function that takes a challenge string and returns a
|
|
13
13
|
* promise that resolves to the solution string.
|
|
14
14
|
*/
|
|
15
|
-
export
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
export function getWorkerApi(): Comlink.Remote<WorkerApi> {
|
|
16
|
+
if (typeof window === "undefined") {
|
|
17
|
+
throw new Error("Worker API can only be used in the browser");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!api) {
|
|
21
|
+
worker = new Worker(new URL("./worker.js", import.meta.url), {
|
|
22
|
+
type: "module",
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
api = Comlink.wrap<WorkerApi>(worker);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return api;
|
|
29
|
+
}
|
package/src/requests.ts
CHANGED
|
@@ -63,7 +63,7 @@ export function get<T>(url: string, options?: RequestInit): Promise<T> {
|
|
|
63
63
|
*/
|
|
64
64
|
export function post<T>(
|
|
65
65
|
url: string,
|
|
66
|
-
options?: Omit<RequestInit, "body" | "method"> & { body
|
|
66
|
+
options?: Omit<RequestInit, "body" | "method"> & { body?: any },
|
|
67
67
|
): Promise<T> {
|
|
68
68
|
return request(url, {
|
|
69
69
|
method: "POST",
|