Add GoodnotesPro tweak

This commit is contained in:
√(noham)²
2026-09-02 12:58:30 +02:00
parent e85614a255
commit 57ab6e6a8f
8 changed files with 248 additions and 1 deletions

1
.gitignore vendored
View File

@@ -3,7 +3,6 @@ Build.md
/RedditPatch
/PineHeartsUnlock
/App
/GoodnotesPro
/StravaPremium
scripts/build_install_loop.sh
CanardDumper/Tweak.x.bak

3
GoodnotesPro/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.theos/
packages/
.DS_Store

View File

@@ -0,0 +1,9 @@
{
Filter = {
Bundles = (
"com.goodnotesapp.x",
"com.goodnotesapp.goodnotes",
"com.TimeBase.TimeBase"
);
};
}

13
GoodnotesPro/Makefile Normal file
View File

@@ -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

181
GoodnotesPro/Tweak.x Normal file
View File

@@ -0,0 +1,181 @@
#import <Foundation/Foundation.h>
// 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");
}

9
GoodnotesPro/control Normal file
View File

@@ -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)

32
GoodnotesPro/index.md Normal file
View File

@@ -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/<user_id>`) 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
```

View File

@@ -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