diff --git a/.gitignore b/.gitignore index f385b75..2b2fb22 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ Build.md /RedditPatch /PineHeartsUnlock /App -/GoodnotesPro /StravaPremium scripts/build_install_loop.sh CanardDumper/Tweak.x.bak diff --git a/GoodnotesPro/.gitignore b/GoodnotesPro/.gitignore new file mode 100644 index 0000000..faf8687 --- /dev/null +++ b/GoodnotesPro/.gitignore @@ -0,0 +1,3 @@ +.theos/ +packages/ +.DS_Store diff --git a/GoodnotesPro/GoodnotesPro.plist b/GoodnotesPro/GoodnotesPro.plist new file mode 100644 index 0000000..a85f8fa --- /dev/null +++ b/GoodnotesPro/GoodnotesPro.plist @@ -0,0 +1,9 @@ +{ + Filter = { + Bundles = ( + "com.goodnotesapp.x", + "com.goodnotesapp.goodnotes", + "com.TimeBase.TimeBase" + ); + }; +} diff --git a/GoodnotesPro/Makefile b/GoodnotesPro/Makefile new file mode 100644 index 0000000..26d85a5 --- /dev/null +++ b/GoodnotesPro/Makefile @@ -0,0 +1,13 @@ +TARGET = iphone:latest:15.0 +INSTALL_TARGET_PROCESSES = GoodNotes +ARCHS = arm64 arm64e + +include $(THEOS)/makefiles/common.mk + +TWEAK_NAME = GoodnotesPro + +GoodnotesPro_FILES = Tweak.x +GoodnotesPro_CFLAGS = -fobjc-arc +GoodnotesPro_FRAMEWORKS = Foundation + +include $(THEOS_MAKE_PATH)/tweak.mk diff --git a/GoodnotesPro/Tweak.x b/GoodnotesPro/Tweak.x new file mode 100644 index 0000000..105b466 --- /dev/null +++ b/GoodnotesPro/Tweak.x @@ -0,0 +1,181 @@ +#import + +// GoodnotesPro — BadNotes port (c22dev) +// https://github.com/c22dev/badnotes + +#pragma mark - Helpers + +static NSString *randomTransactionID(void) { + static NSString *cached = nil; + static dispatch_once_t once; + dispatch_once(&once, ^{ + NSMutableString *s = [NSMutableString stringWithFormat:@"%u", arc4random_uniform(9) + 1]; + for (int i = 0; i < 14; i++) [s appendFormat:@"%u", arc4random_uniform(10)]; + cached = [s copy]; + }); + return cached; +} + +static NSString *subscriberUID(NSString *url) { + if (!url) return nil; + NSRange r = [url rangeOfString:@"/v1/subscribers/"]; + if (r.location == NSNotFound) return nil; + NSString *tail = [url substringFromIndex:r.location + r.length]; + NSRange q = [tail rangeOfString:@"?"]; + if (q.location != NSNotFound) tail = [tail substringToIndex:q.location]; + if (tail.length == 0 || [tail rangeOfString:@"/"].location != NSNotFound) return nil; + return tail; +} + +static NSData *injectEntitlements(NSData *original, NSString *uid) { + static NSString *farFuture = @"2099-12-31T23:59:59Z"; + static NSString *past = @"2025-01-01T00:00:00Z"; + + NSString *productId = @"com.goodnotes.pro_7dt_1y_3599"; + NSString *lifetimeId = @"com.goodnotes.gn6_one_time_unlock_3999"; + + NSMutableDictionary *root = nil; + if (original.length) { + id parsed = [NSJSONSerialization JSONObjectWithData:original options:0 error:nil]; + if ([parsed isKindOfClass:[NSDictionary class]]) root = [parsed mutableCopy]; + } + if (!root) root = [NSMutableDictionary dictionary]; + + NSMutableDictionary *sub = [root[@"subscriber"] isKindOfClass:[NSDictionary class]] + ? [root[@"subscriber"] mutableCopy] + : [NSMutableDictionary dictionary]; + + if (!sub[@"original_app_user_id"]) sub[@"original_app_user_id"] = uid ?: @"app_user"; + if (!sub[@"first_seen"]) sub[@"first_seen"] = past; + sub[@"last_seen"] = @"2025-01-02T13:37:37Z"; + if (!sub[@"non_subscriptions"]) sub[@"non_subscriptions"] = @{}; + if (!sub[@"other_purchases"]) sub[@"other_purchases"] = @{}; + if (!sub[@"original_application_version"]) sub[@"original_application_version"] = @"7"; + if (!sub[@"original_purchase_date"]) sub[@"original_purchase_date"] = past; + + NSDictionary *access = @{ + @"expires_date": farFuture, + @"grace_period_expires_date": [NSNull null], + @"product_identifier": productId, + @"purchase_date": past, + @"original_purchase_date": past, + @"plan_key": @"pro", + }; + + sub[@"entitlements"] = @{ + @"apple_access": access, + @"crossplatform_access": access, + }; + + sub[@"current_plans"] = @{ + @"base": @{@"product_identifier": productId, @"type": @"subscription", @"plan_key": @"pro"}, + @"ai": [NSNull null], + }; + + NSDictionary *subscription = @{ + @"is_sandbox": @NO, + @"ownership_type": @"PURCHASED", + @"expires_date": farFuture, + @"original_purchase_date": past, + @"period_type": @"normal", + @"purchase_date": past, + @"store": @"app_store", + @"store_transaction_id": randomTransactionID(), + @"unsubscribe_detected_at": [NSNull null], + @"grace_period_expires_date": [NSNull null], + @"billing_issues_detected_at": [NSNull null], + @"refunded_at": [NSNull null], + @"auto_resume_date": [NSNull null], + @"plan_key": @"pro", + @"pending_product_id": productId, + @"pending_product_plan_key": @"pro", + @"pending_subscription_period": @"P1Y", + @"quotas": @{@"ai": @{@"quota": @525, @"period": @"P1M"}}, + @"management_url": @"https://apps.apple.com/account/subscriptions", + @"renewal_type": @"auto", + @"subscription_period": @"P1Y", + @"app_type": @"ios", + }; + + sub[@"subscriptions"] = @{ + productId: subscription, + lifetimeId: @{ + @"expires_date": farFuture, + @"original_purchase_date": past, + @"purchase_date": past, + @"ownership_type": @"PURCHASED", + @"store": @"app_store", + }, + }; + + sub[@"quotas"] = @{@"ai": @{@"quota": @525, @"period": @"P1M"}}; + sub[@"management_url"] = @"https://apps.apple.com/account/subscriptions"; + + root[@"subscriber"] = sub; + root[@"request_date"] = @"2025-01-01T13:37:37Z"; + root[@"request_date_ms"] = @1735732800000; + + NSData *out = [NSJSONSerialization dataWithJSONObject:root options:0 error:nil]; + return out ?: original; +} + +#pragma mark - Hooks + +%hook NSBundle +- (NSString *)localizedStringForKey:(NSString *)key value:(NSString *)value table:(NSString *)tableName { + static NSDictionary *overrides = nil; + static dispatch_once_t once; + dispatch_once(&once, ^{ + overrides = @{ + @"subscription_billing_period_yearly": @"BadNotes", + @"subscription_details_subtitle_pro": @"Cracked by Constantin Clerc (c22dev) :)", + }; + }); + NSString *override = overrides[key]; + return override ?: %orig; +} +%end + +%hook NSURLSession +- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request completionHandler:(void (^)(NSData *, NSURLResponse *, NSError *))completionHandler { + NSString *url = request.URL.absoluteString; + BOOL isOfferings = [url containsString:@"offerings"]; + NSString *uid = isOfferings ? nil : subscriberUID(url); + if (!uid && !isOfferings && [url containsString:@"receipt"]) uid = @"app_user"; + + NSURLRequest *reqToUse = request; + if (uid) { + NSMutableURLRequest *mr = [request mutableCopy]; + [mr setValue:nil forHTTPHeaderField:@"If-None-Match"]; + [mr setValue:nil forHTTPHeaderField:@"If-Modified-Since"]; + [mr setValue:nil forHTTPHeaderField:@"X-RevenueCat-ETag"]; + mr.cachePolicy = NSURLRequestReloadIgnoringLocalAndRemoteCacheData; + reqToUse = mr; + } + + void (^wrapped)(NSData *, NSURLResponse *, NSError *) = ^(NSData *data, NSURLResponse *resp, NSError *err) { + if (uid && !err) { + NSData *newData = injectEntitlements([data isKindOfClass:[NSData class]] ? data : nil, uid); + NSURLResponse *respToUse = resp; + if ([resp isKindOfClass:[NSHTTPURLResponse class]]) { + NSHTTPURLResponse *hr = (NSHTTPURLResponse *)resp; + NSMutableDictionary *hdrs = [[hr allHeaderFields] mutableCopy] ?: [NSMutableDictionary dictionary]; + for (NSString *k in hdrs.allKeys) + if ([k caseInsensitiveCompare:@"ETag"] == NSOrderedSame) [hdrs removeObjectForKey:k]; + hdrs[@"Content-Type"] = @"application/json"; + hdrs[@"Content-Length"] = @(newData.length).stringValue; + respToUse = [[NSHTTPURLResponse alloc] initWithURL:hr.URL statusCode:200 HTTPVersion:@"HTTP/1.1" headerFields:hdrs]; + } + if (completionHandler) completionHandler(newData, respToUse, err); + return; + } + if (completionHandler) completionHandler(data, resp, err); + }; + + return %orig(reqToUse, wrapped); +} +%end + +%ctor { + NSLog(@"[GoodnotesPro] Loaded — BadNotes port by c22dev"); +} diff --git a/GoodnotesPro/control b/GoodnotesPro/control new file mode 100644 index 0000000..1694686 --- /dev/null +++ b/GoodnotesPro/control @@ -0,0 +1,9 @@ +Package: xyz.nohamr.goodnotespro +Name: GoodnotesPro +Version: 1.0.0 +Architecture: iphoneos-arm +Description: Unlocks GoodNotes Pro features (BadNotes port). +Maintainer: NohamR +Author: NohamR +Section: Tweaks +Depends: mobilesubstrate (>= 0.9.5000) diff --git a/GoodnotesPro/index.md b/GoodnotesPro/index.md new file mode 100644 index 0000000..85b9fe3 --- /dev/null +++ b/GoodnotesPro/index.md @@ -0,0 +1,32 @@ +# GoodnotesPro + +Unlocks GoodNotes Pro features — port of [BadNotes](https://github.com/c22dev/badnotes) by [c22dev (Constantin Clerc)](https://github.com/c22dev). + +- **App**: [Goodnotes: AI Notes, Docs, PDF](https://apps.apple.com/fr/app/goodnotes-ai-notes-docs-pdf/id1444383602) +- **Bundle**: `com.goodnotesapp.x` (also filters `com.goodnotesapp.goodnotes`, `com.TimeBase.TimeBase` for legacy) +- **Tested on**: GoodNotes 7.1.18 (iOS 18+) +- **Original source**: [`c22dev/badnotes/Tweak.m`](https://github.com/c22dev/badnotes/blob/main/Tweak.m) (v0.2) + +## How it works + +GoodNotes uses RevenueCat (proxied via `*/v1/subscribers/`) to check subscription status. The tweak hooks `NSURLSession -dataTaskWithRequest:completionHandler:` at runtime, strips cache headers (`If-None-Match`, `X-RevenueCat-ETag`), and replaces the JSON response with a crafted payload that grants: + +- `entitlements.apple_access` + `crossplatform_access` → `com.goodnotes.pro_7dt_1y_3599` (expires `2099-12-31`) +- `subscriptions` → both `com.goodnotes.pro_7dt_1y_3599` and legacy `com.goodnotes.gn6_one_time_unlock_3999` +- `quotas.ai` → 525/month +- `current_plans.base` → `pro` + +It also patches `NSBundle -localizedStringForKey:value:table:` to show `BadNotes` labels (yearly period + subtitle). + +> Some server-side features (AI, collaboration) may still not work. + +```sh +make clean && make package THEOS_PACKAGE_SCHEME=rootless DEBUG=0 +``` + +```sh +cyan -i com.goodnotesapp.x-7.1.18.ipa \ + -o com.goodnotesapp.x-7.1.18_patched.ipa \ + -f packages/xyz.nohamr.goodnotespro_1.0.0_iphoneos-arm64.deb \ + -u +``` \ No newline at end of file diff --git a/README.md b/README.md index 98bc310..81835ed 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ iOS tweaks built with [Theos](https://theos.dev), injected into IPAs via [cyan]( | [RMHook](RMHook/index.md) | reMarkable | iOS | | [HatchDragons](HatchDragons/index.md) | HatchDragons 1.2.1 | iOS 18+ | | [BGSessionFix](BGSessionFix/index.md) | BGSessionFix | iOS 15+ | +| [GoodnotesPro](GoodnotesPro/index.md) | GoodnotesPro | iOS 18+ | ## Build