Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exclude files or code from obfuscation #31

Closed
wants to merge 11 commits into from

Conversation

hadiidbouk
Copy link
Contributor

@hadiidbouk hadiidbouk commented Nov 7, 2018

In this PR :

1 - Exclude files from obfuscation using -ignore-files (please not this will only obfuscate the class/struct.. name).
2 - Exclude properties/variables/classes.. that start with a tag using excluded-prefix-tag.
3 - Exclude properties/variables/classes.. that end with a tag using excluded-suffix-tag.
4 - Enable properties obfuscations.

Copy link
Owner

@rockbruno rockbruno left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks interesting! I have a few questions:

  • What -ignore-files is doing is ignore registering a symbol if the name matches the name of an "ignored file", did you mean to ignore files completely? If so, is there a reason to do so? That would only compile correctly if your file doesn't reference anything from other files.
  • Ignoring specific symbols is great, but prefixes and suffixes sounds like something super specific that people wouldn't normally do, what if we made a single -ignore-symbols instead?

@rockbruno
Copy link
Owner

Some new tests would also be great 🙂

@hadiidbouk
Copy link
Contributor Author

hadiidbouk commented Nov 7, 2018

  • -ignore-files I agree with you. The goal was to make a public class with all the methods available to the user, this can be done easily if we have information about the access control (e.g Ignore obfuscation of all public access control). I searched a lot and there Is no way to use reflection and getting the access controls.

There is two solution came to my mind :

  1. Add a JSON file that contains the class name and all the functions inside this class that shouldn't obfuscate. but in this way, we should know the variable/function parent. like a method bla() is inside the class Foo - because we cannot just ignore obfuscation of all the methods that named bla.
  2. The second way, Is to add tags to all public apis/variables/classes.. that should be visible to the user and use the exclude-prefix(suffix)-tag to ignore these from being obfuscated. And when the obfuscation finishes we can clear the added tag from these APIs.

What do you recommend?

  • Some libraries add a prefix to each class containing the company name or the pod name, and some tags can be as a suffix like the __s added in this library.

@rockbruno
Copy link
Owner

You can retrieve access control - I don't have it from the top of my head but if you use -print-sourcekit-queries you can see that SourceKit adds info if something is public. That will allow SDKs to ignore public stuff. Will take some tinkering but it's definitely feasible. I actually want to drop the whole tag thing because it's causes a lot of problems with someone random library accidentally name something like your tag.

@hadiidbouk
Copy link
Contributor Author

hadiidbouk commented Nov 7, 2018

Also why static properties are not obfuscated?

public static let xxxxxxxxxxxxxxxxxxxxxxxxxxxxx__npc7a: Double = 5

the xx..xx is my property name, but it's not obfuscated.

It seems a problem with static let because static var worked just fine

@rockbruno
Copy link
Owner

Can you run your code with -print-sourcekit-queries and see if there's any difference when your property is a let? It might use an identifier that is not mapped in SwiftShield.

@hadiidbouk
Copy link
Contributor Author

hadiidbouk commented Nov 7, 2018

{removed large output}

@hadiidbouk
Copy link
Contributor Author

private final let is working.
The problem with static access control.

@rockbruno
Copy link
Owner

Sorry, it's actually -show-sourcekit-queries. They look like this:

          key.entities: [
            {
              key.kind: source.lang.swift.ref.class,
              key.name: "B",
              key.usr: "s:28index_is_test_candidate_objc1BC",
              key.line: 25,
              key.column: 31
            },]

@hadiidbouk
Copy link
Contributor Author

hadiidbouk commented Nov 7, 2018

{removed large output}

@rockbruno
Copy link
Owner

Can you move this output to a pastebin or something like that so this page doesn't get cluttered?
The problem is that your static properties are indexed as var.static, but in this line: https://github.com/rockbruno/swiftshield/pull/31/files#diff-ee2848e911891c82d85100c000e42012R30

you only check for var.instance and var.class :)

@hadiidbouk
Copy link
Contributor Author

Thank you!

@hadiidbouk
Copy link
Contributor Author

hadiidbouk commented Nov 12, 2018

@rockbruno @vrujbr please check the edited files

Copy link
Owner

@rockbruno rockbruno left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome! I haven't tested it yet but the shape of it looks exactly like we need 🙂 I'll just nitpick the structure of the code:

@@ -97,6 +96,20 @@ class AutomaticSwiftShield: Protector {
}
writeToFile(data: data, path: path, info: "Automatic mode for \(path)")
}

func removeSuffixTags() {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not understand what this is supposed to be doing, is it a leftover from something?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leftover, I will remove it.

let filesToIgnore: Set<String>
let excludedPrefixTag: String
let excludedSuffixTag: String
var publicProtocols: Set<String>!
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not use force unwrap - If storing the protocols here are needed, we can pre-init it for safety reasons 🙂
var publicProtocols: Set<String> = []

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok


if self.excludedSuffixTag != "" && name.hasSuffix(excludedSuffixTag) {
return nil
if excludePublic {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid making getNameData get super big, we can move this logic to a separate shouldIgnoreSymbol()-like method, makes sense?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok

let attributesData = dict.getAttributes(dict: attributesDict, subKey: sourceKit.attributeID)


//Check if variant is public
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's avoid comments unless explaining an intent is needed - what is happening should be clear enough through the name of the properties we're dealing with.


//Check if variant is public
let isPublic = attributesData.filter { item in
return item.contains("public")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would avoid dealing with strings directly. We can make an enum to handle visibility identifiers. I already did that for kinds, so we can follow the same pattern 🙂

}.count != 0

//Add to publicProtocols array
if kind == "source.lang.swift.decl.protocol" && isPublic {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing here (not dealing with strings directly), SKAPI already has a method to return what an identifier is - I think we just need to add the protocol case.


//Handle public protocol's functions
for protocolName in publicProtocols {
if usr.contains(protocolName) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understood this correctly, shouldn't this be simply publicProtocols.contains(usr) instead of a loop? This is running in O(nˆ2)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, because the receiver_usr isn't returning just a name, It's something like that
s:17ObfuscationSource14ViewControllerC5countSivp

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see now. The problem is that using contains inside an usr will likely not work in a bigger app because nothing stops other modules from having private protocols with the same name as your public one. I think you need to store the protocol usrs in publicProtocols instead of their names to avoid this loop.

But I'm getting some bad vibes, is this deterministic? I'm having the impression that a public protocol's method would be wrongly obfuscated if the order of the files changed. If you look at isReferencingInternal(), I had to even make it recursive to make it work. Things are slightly more difficult here because it happens before everything, but I think it's gonna work nicely in the end.

guard let protected = obfuscationData.obfuscationDict[name] else {
//if !shouldRemoveSuffixTags {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can remove these leftovers

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will remove it.

@@ -57,4 +55,9 @@ protector.protectStoryboards(data: obfuscationData)
protector.writeToFile(data: obfuscationData)
protector.markProjectsAsProtected()
Logger.log(.finished)

if automatic && CommandLine.arguments.contains("-clear-suffix-tags") {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's also a leftover right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leftover, I will remove it.


var data = [String]()

let _ = SKApi.sourcekitd_variant_array_apply(dict) { (_, attributesDict) in
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this safe? Wouldn't it crash on none-array dict?

@rockbruno rockbruno mentioned this pull request Nov 25, 2018
@rockbruno
Copy link
Owner

I took this weekend to run the property obfuscation feature (and to implement the public-ignoring myself to see if I could do it) to see if there no hidden problems in it, and I found a couple 😢

In regards to obfuscating properties, it turns out that it really messes up Codable. Because most people use it to parse JSONs from the web, changing the properties will break parsing. The fix for this is to explicitely define Codable's keys through CodingKeys, but unfortunately people don't do this due to Codable's auto-derivation properties. I think there are some SourceKit bugs as well because some properties weren't being indexed (It could be an error on my side though. Haven't checked it deeply)

Regarding avoiding publics, I tried to do it and faced many problems, including some we haven't noticed before, which is that extensions also share the protocol problems:

// Both are valid. bar() is public in both
extension Foo {
    public func bar() {}
}

public extension Foo {
    func bar()
}

SourceKit isn't very helpful here since it does not mark bar() as public in the latter one, although you could find this out since it's inside a public extension. There's nothing in the tool that does this kind of backwards check though, and I think blindly checking for publics in previous scopes would return you some false positives (internal method in public class isn't public).

I would deeply investigate all these edge cases before trying to develop this again, but I still believe it's possible. I unfortunately do not know what to do in regards to properties though.

@vrujbr
Copy link

vrujbr commented Nov 26, 2018

Great work @rockbruno! To these edge cases..
I say don't worry about the extensions. Mention in readme that public methods in public extensions have to be specifically marked public in order for them not be obfuscated with omit public argument.
With the properties it's a bit worse. I guess you could do the same and force people to use coding keys. Or omit Codable all together?

case "var.instance",
"var.static",
"var.class":
return .property
case "function.free",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you enable property, confirm no storyboard property in use

@hadiidbouk
Copy link
Contributor Author

Any update on this? Is there any plan for the future for ignoring public methods?

@rockbruno
Copy link
Owner

My previous comment still holds true, SourceKit doesn't cover all ways to make something public and things that are derived need to be ignored, so I don't have a good outlook for this 😢

@rockbruno
Copy link
Owner

The tool has been remade. SDK mode exists, and although the bugs weren't fixed, we detailed them in the section. The exclusion part can still be done.

@rockbruno rockbruno closed this May 14, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
4 participants