Skip to content

Commit

Permalink
add support for advanced data protection
Browse files Browse the repository at this point in the history
  • Loading branch information
foxt authored Jan 23, 2023
1 parent df2130c commit 824edf4
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 16 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 10 additions & 8 deletions src/authStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ export default class iCloudAuthenticationStore {
constructor(options: iCloudServiceSetupOptions) {
this.options = options;
this.tknFile = path.format({ dir: options.dataDirectory, base: ".trust-token" });
this.loadTrustToken();

Object.defineProperty(this, "trustToken", { enumerable: false });
Object.defineProperty(this, "sessionId", { enumerable: false });
Expand All @@ -30,17 +29,17 @@ export default class iCloudAuthenticationStore {
Object.defineProperty(this, "icloudCookies", { enumerable: false });
}

loadTrustToken() {
loadTrustToken(account: string) {
try {
this.trustToken = fs.readFileSync(this.tknFile, "utf8");
this.trustToken = fs.readFileSync(this.tknFile + "-" + Buffer.from(account.toLowerCase()).toString("base64"), "utf8");
} catch (e) {
console.debug("[icloud] Unable to load trust token:", e.toString());
}
}
writeTrustToken() {
writeTrustToken(account: string) {
try {
if (!fs.existsSync(this.options.dataDirectory)) fs.mkdirSync(this.options.dataDirectory);
require("fs").writeFileSync(this.tknFile, this.trustToken);
require("fs").writeFileSync(this.tknFile + "-" + Buffer.from(account.toLowerCase()).toString("base64"), this.trustToken);
} catch (e) {
console.warn("[icloud] Unable to write trust token:", e.toString());
}
Expand Down Expand Up @@ -72,18 +71,21 @@ export default class iCloudAuthenticationStore {
.filter((v) => !!v);
return !!this.icloudCookies.length;
}
processAccountTokens(trustResponse: Response) {
processAccountTokens(account:string, trustResponse: Response) {
this.sessionToken = trustResponse.headers.get("x-apple-session-token");
this.trustToken = trustResponse.headers.get("x-apple-twosv-trust-token");
this.writeTrustToken();
this.writeTrustToken(account);
return this.validateAccountTokens();
}
addCookies(cookies: string[]) {
cookies.map((v) => Cookie.parse(v)).forEach((v) => this.icloudCookies.push(v));
}

getMfaHeaders() {
return { ...AUTH_HEADERS, scnt: this.scnt, "X-Apple-ID-Session-Id": this.sessionId, Cookie: "aasp=" + this.aasp };
}
getHeaders() {
return { ...DEFAULT_HEADERS, Cookie: this.icloudCookies.map((cookie) => cookie.cookieString()).join("; ") };
return { ...DEFAULT_HEADERS, Cookie: this.icloudCookies.filter((a) => a.value).map((cookie) => cookie.cookieString()).join("; ") };
}

validateAccountTokens() {
Expand Down
100 changes: 94 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export const enum iCloudServiceStatus {
Error = "Error"
}



export interface iCloudStorageUsage {
storageUsageByMedia: Array<{
mediaKey: string
Expand Down Expand Up @@ -69,12 +71,31 @@ export interface iCloudStorageUsage {
}


function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

export default class iCloudService extends EventEmitter {
authStore: iCloudAuthenticationStore;
options: iCloudServiceSetupOptions;

status: iCloudServiceStatus = iCloudServiceStatus.NotStarted;

/*
Has PCS (private/protected cloud service?) enabled.
The check is implemented by checking if the `isDeviceConsentedForPCS` key is present in the `requestWebAccessState` object.
*/
pcsEnabled?: boolean;
/**
* PCS access is granted.
*/
pcsAccess?: boolean;
/**
* Has ICRS (iCloud Recovery Service) disabled.
* This should only be true when iCloud Advanced Data Protection is enabled.
*/
ICDRSDisabled?: boolean;

accountInfo?: AccountInfo;

awaitReady = new Promise((resolve, reject) => {
Expand Down Expand Up @@ -129,6 +150,7 @@ export default class iCloudService extends EventEmitter {


if (!fs.existsSync(this.options.dataDirectory)) fs.mkdirSync(this.options.dataDirectory);
this.authStore.loadTrustToken(this.options.username);



Expand Down Expand Up @@ -193,20 +215,15 @@ export default class iCloudService extends EventEmitter {
AUTH_ENDPOINT + "2sv/trust",
{ headers: this.authStore.getMfaHeaders() }
);
if (this.authStore.processAccountTokens(authResponse)) {
if (this.authStore.processAccountTokens(this.options.username, authResponse)) {
this._setState(iCloudServiceStatus.Trusted);
} else {
console.error("[icloud] Unable to trust device!");
}
}




async getiCloudCookies() {
if (!this.authStore.validateAccountTokens()) {
throw new Error("Cannot get iCloud cookies because some tokens are missing.");
}
try {
const data = {
dsWebAuthToken: this.authStore.sessionToken,
Expand All @@ -220,6 +237,14 @@ export default class iCloudService extends EventEmitter {
} catch (e) {
console.warn("[icloud] Could not get account info:", e);
}

try {
await this.checkPCS();
} catch (e) {
console.warn("[icloud] Could not get PCS state:", e);
}


this._setState(iCloudServiceStatus.Ready);
try {
require("keytar").setPassword("https://idmsa.apple.com", this.options.username, this.options.password);
Expand All @@ -238,6 +263,69 @@ export default class iCloudService extends EventEmitter {
}
}








async checkPCS() {
const pcsTest = await fetch("https://setup.icloud.com/setup/ws/1/requestWebAccessState", { headers: this.authStore.getHeaders(), method: "POST" });
if (pcsTest.status == 200) {
const j = await pcsTest.json();
this.pcsEnabled = typeof j.isDeviceConsentedForPCS == "boolean";
this.pcsAccess = this.pcsEnabled ? j.isDeviceConsentedForPCS : true;
this.ICDRSDisabled = j.isICDRSDisabled || false;
}
}

async requestServiceAccess(appName: string) {
await this.checkPCS();
if (!this.ICDRSDisabled) {
console.warn("[icloud] requestServiceAccess: ICRS is not disabled.");
return true;
}
if (!this.pcsAccess) {
const requestPcs = await fetch("https://setup.icloud.com/setup/ws/1/enableDeviceConsentForPCS", { headers: this.authStore.getHeaders(), method: "POST" });
const requestPcsJson = await requestPcs.json();
if (!requestPcsJson.isDeviceConsentNotificationSent) {
throw new Error("Unable to request PCS access!");
}
}
while (!this.pcsAccess) {
await sleep(5000);
await this.checkPCS();
}
let pcsRequest = await fetch("https://setup.icloud.com/setup/ws/1/requestPCS", { headers: this.authStore.getHeaders(), method: "POST", body: JSON.stringify({ appName, derivedFromUserAction: true }) });
let pcsJson = await pcsRequest.json();
while (true) {
if (pcsJson.status == "success") {
break;
} else {
switch (pcsJson.message) {
case "Requested the device to upload cookies.":
case "Cookies not available yet on server.":
await sleep(5000);
break;
default:
console.error("[icloud] unknown PCS request state", pcsJson);
}
pcsRequest = await fetch("https://setup.icloud.com/setup/ws/1/requestPCS", { headers: this.authStore.getHeaders(), method: "POST", body: JSON.stringify({ appName, derivedFromUserAction: false }) });
pcsJson = await pcsRequest.json();
}
}
this.authStore.addCookies(pcsRequest.headers.raw()["set-cookie"]);

return true;
}







private _serviceCache: {[key: string]: any} = {};
serviceConstructors: {[key: string]: any} = {
account: iCloudAccountDetailsService,
Expand Down
1 change: 1 addition & 0 deletions test/drive.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const authenticate = require("./authenticate");
const input = require("input");

authenticate.then(async(icloud) => {
await icloud.requestServiceAccess("iclouddrive");
const driveService = icloud.getService("drivews");
let root = await driveService.getNode();
while (true) {
Expand Down

0 comments on commit 824edf4

Please sign in to comment.