import * as Cookie from "./Cookies.js"; export async function GetBlueskyDID(PDS, Handle) { let DPoP = await ClientDPoPPDS("GET", PDS + "/xrpc/com.atproto.identity.resolveHandle?handle=" + Handle); let request = fetch(PDS + "/xrpc/com.atproto.identity.resolveHandle?handle=" + Handle, { method: "GET", headers: {"Authorization": "DPoP " + Cookie.BlueskyAccessTokenCookie, "DPoP": DPoP}}); let body = await request.then((response) => response.json()); let status = await request.then((response) => response.status); let header = await request.then((response) => response.headers.get("dpop-nonce")); if (status == 401) { await FixNonceMismatch(header); request = fetch(PDS + "/xrpc/com.atproto.identity.resolveHandle?handle=" + Handle, { method: "GET", headers: {"Authorization": "DPoP " + Cookie.BlueskyAccessTokenCookie, "DPoP": DPoP}}); body = await request.then((response) => response.json()); } return body; } export async function CreatePost(PDS, DID, Text) { let Json = { "$type": "app.bsky.feed.post", "text": Text, "createdAt": new Date(Date.now()).toISOString() } let RequestBody = { "repo": DID, "collection": "app.bsky.feed.post", "record": Json } console.log(DID); console.log(RequestBody.repo); let DPoP = await ClientDPoPPDS("POST", PDS + "/xrpc/com.atproto.repo.createRecord"); let request = fetch(PDS + "/xrpc/com.atproto.repo.createRecord", { body: JSON.stringify(RequestBody), method: "POST", headers: {"Content-Type": "application/json", "Authorization": "DPoP " + Cookie.BlueskyAccessTokenCookie, "DPoP": DPoP}}); let body = await request.then((response) => response.json()); let status = await request.then((response) => response.status); let header = await request.then((response) => response.headers.get("dpop-nonce")); if (status == 401) { await FixNonceMismatch(header); let request = fetch(PDS + "/xrpc/com.atproto.repo.createRecord", { body: JSON.stringify(RequestBody), method: "POST", headers: {"Content-Type": "application/json", "Authorization": "DPoP " + Cookie.BlueskyAccessTokenCookie, "DPoP": DPoP}}); body = await request.then((response) => response.json()); } return body; } // Added after all the components: in case of nonce mismatch... export async function FixNonceMismatch(head) { return Cookie.InputCookie(Cookie.BlueskyNonceName, head); } // Component 1/4 export async function GetPDSWellKnown() { return await fetch("https://bsky.social/.well-known/oauth-authorization-server", {method: "GET"}) .then((response) => response.json()); } // Component 2/4 // Many thanks to https://github.com/tonyxu-io/pkce-generator. It was the base for this code. export async function CreatePKCECodeVerifier() { // Generate some Numbers let Numbers = new Uint8Array(32); crypto.getRandomValues(Numbers); // Generate a random string of characters. let CodeVerifier = ""; for (let i in Numbers) { CodeVerifier += String.fromCharCode(Numbers[i]); } // Put this random string into Base64URL format. CodeVerifier = btoa(CodeVerifier).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); return CodeVerifier; } export async function CreatePKCECodeChallenge(CodeVerifier) { // Generate a code challenge with the code verifier. // This is done by first SHA256 encrypting the CodeVerifier, then putting the outputted string into Base64URL format. let CodeChallenge = btoa(await sha256(CodeVerifier)).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); return CodeChallenge; } // Component 3/4 export async function PARrequest(PAREndpoint, State, Challenge) { return fetch(PAREndpoint, {method: "POST", body: new URLSearchParams({ response_type: "code", code_challenge_method: "S256", scope: "atproto transition:generic", client_id: "https://fedi.crowdedgames.group/oauth/client-metadata.json", redirect_uri: "https://fedi.crowdedgames.group/HTML/setting.html", code_challenge: Challenge, state: State, login_hint: "crowdedgames.group" }), headers: {"Content-Type": "application/x-www-form-urlencoded"}}); } export async function AuthRequest(TokenEndpoint, code, DPoP, Verify) { return fetch(TokenEndpoint, {method: "POST", body: new URLSearchParams({ grant_type: "authorization_code", code: code, client_id: "https://fedi.crowdedgames.group/oauth/client-metadata.json", redirect_uri: "https://fedi.crowdedgames.group/HTML/setting.html", code_verifier: Verify}), headers: { "DPoP": DPoP, "Content-Type": "application/x-www-form-urlencoded"}}) .then((response) => response.json()); } // Component 4/4 export async function ClientDPoPToken(POSTorGET, RequestURL) { let PublicKey = await crypto.subtle.importKey("jwk", JSON.parse(Cookie.BlueskyPublicKeyCookie), {name: "ECDSA", namedCurve: "P-256"}, true, ["verify"]); let PrivateKey = await crypto.subtle.importKey("jwk", JSON.parse(Cookie.BlueskyPrivateKeyCookie), {name: "ECDSA", namedCurve: "P-256"}, true, ["sign"]); // Header var Header = {typ: "dpop+jwt", alg: "ES256", jwk: await crypto.subtle.exportKey("jwk", PublicKey) .then(function(response) { delete response["key_ops"]; delete response["ext"]; delete response["alg"]; return response}) }; // Payload var Payload = {}; Payload.iss = "https://fedi.crowdedgames.group/oauth/client-metadata.json"; Payload.jti = crypto.randomUUID(); Payload.htm = POSTorGET; Payload.htu = RequestURL; Payload.iat = Math.floor(new Date(Date.now()).getTime() / 1000); Payload.nonce = Cookie.BlueskyNonceCookie; var sHeader = JSON.stringify(Header); var sPayload = JSON.stringify(Payload); var JWT = KJUR.jws.JWS.sign("ES256", sHeader, sPayload, await crypto.subtle.exportKey("jwk", PrivateKey) .then(function(response) { delete response["key_ops"]; delete response["ext"]; delete response["alg"]; return response}) ); return JWT; } // So far does nothing? Don't touch :3 export async function ClientDPoPPDS(POSTorGET, RequestURL) { let PublicKey = await crypto.subtle.importKey("jwk", JSON.parse(Cookie.BlueskyPublicKeyCookie), {name: "ECDSA", namedCurve: "P-256"}, true, ["verify"]); let PrivateKey = await crypto.subtle.importKey("jwk", JSON.parse(Cookie.BlueskyPrivateKeyCookie), {name: "ECDSA", namedCurve: "P-256"}, true, ["sign"]); // Header var Header = {typ: "dpop+jwt", alg: "ES256", jwk: await crypto.subtle.exportKey("jwk", PublicKey) .then(function(response) { delete response["key_ops"]; delete response["ext"]; delete response["alg"]; return response}) }; // Payload var Payload = {}; Payload.iss = "https://fedi.crowdedgames.group/oauth/client-metadata.json"; Payload.jti = crypto.randomUUID(); Payload.htm = POSTorGET; Payload.htu = RequestURL; Payload.iat = Math.floor(new Date(Date.now()).getTime() / 1000); Payload.nonce = Cookie.BlueskyNonceCookie; Payload.ath = await CreatePKCECodeChallenge(Cookie.BlueskyAccessTokenCookie); var sHeader = JSON.stringify(Header); var sPayload = JSON.stringify(Payload); var JWT = KJUR.jws.JWS.sign("ES256", sHeader, sPayload, await crypto.subtle.exportKey("jwk", PrivateKey) .then(function(response) { delete response["key_ops"]; delete response["ext"]; delete response["alg"]; return response}) ); return JWT; } export async function HandleAuthorization() { // Declare Variables let KeyPair = await crypto.subtle.generateKey({name: "ECDSA", namedCurve: "P-256"}, true, ["sign", "verify"]); let WellKnown = await GetPDSWellKnown(); let State = crypto.randomUUID(); let PKCEverifier = await CreatePKCECodeVerifier(); let PKCEchallenge = await CreatePKCECodeChallenge(PKCEverifier); // Save these Cookie.InputCookie(Cookie.BlueskyPKCEVeriferName, PKCEverifier); Cookie.InputCookie(Cookie.BlueskyPKCEChallengeName, PKCEchallenge); // PAR request (beginning) let PAR = PARrequest(WellKnown.pushed_authorization_request_endpoint, State, PKCEchallenge); let body = await PAR.then((response) => response.json()); let nonce = await PAR.then((response) => response.headers.get("dpop-nonce")); // Save nonce Cookie.InputCookie(Cookie.BlueskyNonceName, nonce); // Export keys let ExportedKey1 = await crypto.subtle.exportKey("jwk", KeyPair.publicKey); let ExportedKey2 = await crypto.subtle.exportKey("jwk", KeyPair.privateKey); // Convert them into a good format. Cookie.InputCookie(Cookie.BlueskyPublicKeyName, JSON.stringify(ExportedKey1)); Cookie.InputCookie(Cookie.BlueskyPrivateKeyName, JSON.stringify(ExportedKey2)); // Now we need to authenticate. Make sure the State stays the same throughout this whole process :] document.location.href = "https://bsky.social/oauth/authorize?client_id=https://fedi.crowdedgames.group/oauth/client-metadata.json&request_uri=" + body.request_uri; } export async function GainTokens() { let WellKnown = await GetPDSWellKnown(); // Check to see if something's a miss... if ((document.location.href.split("state=").length > 1 && document.location.href.split("iss=").length > 1 && document.location.href.split("code=").length > 1) && Cookie.IsCookieReal(Cookie.BlueskyPKCEVeriferCookie) && !(Cookie.IsCookieReal(Cookie.BlueskyAccessTokenCookie))) { // Create varaibles, be aware of waits because of internet. let DPoP = await ClientDPoPToken("POST", WellKnown.token_endpoint); let code = document.location.href.split("code=")[1]; // Authentication let cookie = await Cookie.BlueskyPKCEVeriferCookie; let Auth = await AuthRequest(WellKnown.token_endpoint, code, DPoP, cookie); // Save the tokens and be done Cookie.InputCookie(Cookie.BlueskyAccessTokenName, Auth.access_token); Cookie.InputCookie(Cookie.BlueskyRefreshTokenName, Auth.refresh_token); } } // Stolen from elsewhere. // Firefox snippet; Slightly edited. async function sha256(message) { // encode as UTF-8 const MessageBuffer = new TextEncoder().encode(message); // hash the message const HashBuffer = await crypto.subtle.digest('SHA-256', MessageBuffer); // convert ArrayBuffer to Array const HashArray = Array.from(new Uint8Array(HashBuffer)); // convert this hashArray to a string let string = ""; for (let i in HashArray) { string += String.fromCharCode(HashArray[i]); } return string; } // Firefox snippet. function ab2str(buf) { return String.fromCharCode.apply(null, new Uint8Array(buf)); } // Firefox snippet. function str2ab(str) { const buf = new ArrayBuffer(str.length); const bufView = new Uint8Array(buf); for (let i = 0, strLen = str.length; i < strLen; i++) { bufView[i] = str.charCodeAt(i); } return buf; }