<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Posts on Daniel Dallos</title>
        <link>https://danieldallos.com/posts/</link>
        <description>Recent content in Posts on Daniel Dallos</description>
        <generator>Hugo -- gohugo.io</generator>
        <copyright>&lt;a href=&#34;https://creativecommons.org/licenses/by-nc/4.0/&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;CC BY-NC 4.0&lt;/a&gt;</copyright>
        <lastBuildDate>Fri, 21 Apr 2023 22:12:18 +0200</lastBuildDate>
        <atom:link href="https://danieldallos.com/posts/index.xml" rel="self" type="application/rss+xml" />
        
        <item>
            <title>The dark side of WKWebView.isInspectable</title>
            <link>https://danieldallos.com/posts/2023/04/the-dark-side-of-wkwebview.isinspectable/</link>
            <pubDate>Fri, 21 Apr 2023 22:12:18 +0200</pubDate>
            
            <guid>https://danieldallos.com/posts/2023/04/the-dark-side-of-wkwebview.isinspectable/</guid>
            <description>Web inspection was always a thing on iOS (tvOS, macOS) for WKWebView (if available) and JSContext. It was available for developer-provisioned apps built directly from Xcode for local development.
 However, released versions of apps had no way to inspect dynamic web content or scripts, leaving developers and users to have to resort to more complicated workflows to get information that would otherwise be made available by Web Inspector.
 From iOS 16.</description>
            <content type="html"><![CDATA[<p>Web inspection was always a thing on iOS (tvOS, macOS) for <code>WKWebView</code> (if available) and <code>JSContext</code>. It was available for developer-provisioned apps built directly from Xcode for local development.</p>
<blockquote>
<p>However, released versions of apps had no way to inspect dynamic web content or scripts, leaving developers and users to have to resort to more complicated workflows to get information that would otherwise be made available by Web Inspector.</p>
</blockquote>
<p>From iOS 16.4 (and MacOS 13.3) Apple <a href="https://webkit.org/blog/13936/enabling-the-inspection-of-web-content-in-apps/">introduced</a> <code>isInspectable</code> flag on <a href="https://developer.apple.com/documentation/webkit/wkwebview/4111163-isinspectable"><code>WKWebView</code></a> and on <a href="https://developer.apple.com/documentation/javascriptcore/jscontext/4111147-isinspectable"><code>JSContext</code></a></p>
<h2 id="how-to-enable-inspection">How to enable inspection?</h2>
<p>According to <a href="https://webkit.org/blog/13936/enabling-the-inspection-of-web-content-in-apps/">webkit.org</a>:</p>
<pre><code class="language-swift">let webConfiguration = WKWebViewConfiguration()
let webView = WKWebView(frame: .zero, configuration: webConfiguration)
webView.isInspectable = true
</code></pre>
<p>Or:</p>
<pre><code class="language-swift">let jsContext = JSContext()
jsContext?.isInspectable = true
</code></pre>
<p>And from a desktop Safari's Develop menu you can access now the inspection.</p>
<p>(For iOS and iPadOS, you must also have enabled Web Inspector in the Settings app under Safari &gt; Advanced &gt; Web Inspector. For simulators it is always enabled.)</p>

<figure  class="center" 
    
    >
    
        <img src="/images/darkside-of-inspectable/inspectable.png" alt="Flutter logo" />
    
    
</figure>

<h2 id="the-caveat">The caveat</h2>
<p>If you start your iOS development journey after iOS 16.4 you will rely on the current state of Web inspection.</p>
<p>As <a href="https://webkit.org/blog/13936/enabling-the-inspection-of-web-content-in-apps/">webkit.org</a> also mentions, the <code>isInspectable</code> flag</p>
<blockquote>
<p>defaults to <strong>false</strong>, and you can set it to true to opt-in to content being inspectable. <strong>This decision is made for each individual WKWebView and JSContext to prevent unintentionally making it enabled for a view or context you don’t intend to be inspectable</strong></p>
</blockquote>
<p>This will make you think that disabling the <code>isInspectable</code> flag is enough to restrict the access to your Webview/JSContext for 3rd-parties.</p>
<p><strong>This assumption can make you lazy and you forget to obfuscate your sensitive Javascript code.</strong></p>
<p>This is specially important if you develop (and distribute) a framework that has its business logic in Javascript (and even contains some sensitive data)</p>
<p>Before iOS 16.4 (as mentioned earlier) Web inspection was by default enabled for development-signed builds, so you were alerted immediately once you opened Safari's Develop menu. You had to make precautions against disassembling your code.</p>
<p>This is not the case anymore and your false assumption of safety can cause some pain later.</p>
<h2 id="whats-going-on">What's going on?</h2>
<p>In a previous <a href="https://danieldallos.com/posts/2019/12/how-to-spy-on-your-ios-users-by-using-the-camera/">article</a> I already talked about hooking into (aka. <strong>swizzling</strong>) methods.</p>
<p>From the moment <code>isInspectable</code> is a property on a class, we can use the same technique to override the &ldquo;hardcoded&rdquo; value by the 3rd-party framework (or even by an app on a jailbroken device).</p>
<p>We need to find a hooking point (preferable as late as possible, so our new value can not be overridden by the framework/app anymore), e.g. when the Webview loads an HTML string. (But this can be anything. Setting a certain value, calling a constructor, etc&hellip;)</p>
<pre><code class="language-swift">extension WKWebView {

    @objc dynamic func _swizzled_loadHTMLString(_ string: String, baseURL: URL?) -&gt; WKNavigation? {
        //this will do the magic
        if #available(iOS 16.4, *) {
            print(&quot;[SWIZZLING] Overriding isInspectable from loadHTML&quot;)
            self.isInspectable = true
        } else {
            // No flag on earlier version
        }
        
        // call the original method. Looks like an infinite loop, but the implementations are exchanged!
        return _swizzled_loadHTMLString(string, baseURL: baseURL)
    }
}
</code></pre>
<p>After we found the right method, we can exchange the implementations.</p>
<pre><code class="language-swift">extension WKWebView {
    static func applySwizzling() {
        print(&quot;[SWIZZLING] applySwizzling&quot;)
        
        //original method
        let selector1 = #selector(WKWebView.loadHTMLString(_:baseURL:))
        //new method
        let selector2 = #selector(WKWebView._swizzled_loadHTMLString(_:baseURL:))
        
        let originalMethod = class_getInstanceMethod(WKWebView.self, selector1)!
        let swizzleMethod = class_getInstanceMethod(WKWebView.self, selector2)!
        //exchange implementations
        method_exchangeImplementations(originalMethod, swizzleMethod)
        
    }
}
</code></pre>
<p>We can call <code>WKWebView.applySwizzling()</code> to enable hooking as early as possible in the application life-cycle (e.g. in <code>AppDelegate</code>)</p>
<p>After applying the hooking, we are able to see the Web inspection in Safari again for all the Webview within our app. We can see all of ours, but also all the 3rd-party ones which were not meant to be seen by us.</p>
<h3 id="why-is-this-a-problem">Why is this a problem?</h3>
<ol>
<li>Hidden parts can be discovered of any 3rd-party SDK while using them.</li>
<li>On a jailbroken device we can open up all the Webviews (in any app) by applying the snippet above via a Jailbreak tweak.</li>
<li><code>isInspectable</code> is a public API, so swizzling it is not against the Apple Store guidelines. (So if we find a valid use-case for it, we are free to do it in release apps)</li>
</ol>
<h2 id="can-we-avoid-our-secrets-being-exposed">Can we avoid our secrets being exposed?</h2>
<p>There are few tricks we can try. I am going to touch up on few of these.</p>
<h3 id="always-obfuscate-the-javascript-code">Always obfuscate the Javascript code</h3>
<p>This is the first and most important defense line. If the business logic is obfuscated and hard to read we can already make the bad actor's life difficult.</p>
<p>Also, don't store any secret inside Javascript.</p>
<h3 id="swizzle-the-swizzling">Swizzle the swizzling</h3>
<p>Unfortunately, we can not be faster within a framework than someone who is using our library within an app and can put hooking around application life-cycle methods.</p>
<p>However, if we are lucky we can hook into the right method in the right time, so every call to that method will arrive to us and we can decide how to continue.</p>
<pre><code class="language-swift">    @objc dynamic func _swizzleSetter(_ val: Bool) {
        print(&quot;[INTERNAL SWIZZLING] isInspectable setter &quot;, val)
        _swizzleSetter(val)
    }
    
    static func applySwizzling() {
        print(&quot;[INTERNAL SWIZZLING] applySwizzling&quot;)

        if #available(iOS 16.4, *) {
            let selector11 = #selector(setter: WKWebView.isInspectable)
            let selector21 = #selector(WKWebView._swizzleSetter(_:))
            let originalMethod = class_getInstanceMethod(WKWebView.self, selector11)!
            let swizzleMethod = class_getInstanceMethod(WKWebView.self, selector21)!
            method_exchangeImplementations(originalMethod, swizzleMethod)
        } else {
            // Fallback on earlier versions
        }
    }
</code></pre>
<p>In the sample code above we hook into the setter of <code>isInspectable</code>, so whenever an app-level hooking would try to set <code>isInspectable</code> to <code>TRUE</code> we can just reject it.</p>
<p>So in the first hooking sample snippet, whenever <code>_swizzled_loadHTMLString</code> hook calls <code>self.isInspectable = true</code>, that call will be delivered to <code>_swizzleSetter</code> and there we can decide what to do.</p>
<h3 id="devirtualizing-objective-c-calls">Devirtualizing Objective-C calls</h3>
<p>When calling a method on a class in Objective-C, under the hood it uses <a href="https://developer.apple.com/documentation/objectivec/1456712-objc_msgsend">objc_msgSend</a> which will call (dispatch a message) the selected function via a selector on a specific object (with specific parameters).</p>
<p>Because of this technique the Obj-C methods are called &ldquo;virtual&rdquo; and they are dynamically resolved during runtime.</p>
<p>And this way of communication makes method swizzling work.</p>
<p>With bitcode manipulation we can bypass <code>objc_msgSend</code> and call the required function directly. This is called devirtualization.</p>
<p>There is a <a href="https://www.guardsquare.com/blog/using-llvm-to-prevent-objective-c-swizzling-through-devirtualization">brilliant article from GuardSquare</a> on the topic that explains how it works.</p>
<p><em>Note: Don't try this at home 🙂&hellip; Try to use tools that make your life easier.</em></p>
<h2 id="conclusion">Conclusion</h2>
<p>Obfuscate and keep challenging the conventional wisdom!</p>
<h2 id="support">Support</h2>
<p>Did you enjoy my story? There is more in the pipeline&hellip; 😉</p>
<p>Do you want to know more insights? Would you like to discuss the used techniques, or would you like to see some part of the code?
Then consider becoming a <strong>monthly supporter</strong> on Patreon via my <strong>Tweaked.Tech</strong> initiative.</p>



<style>
    .bmc-button img {
        height: 30px !important;
        width: 30px !important;
        margin-bottom: 1px !important;
        box-shadow: none !important;
        border: none !important;
        vertical-align: middle !important;
    }

    .bmc-button {
        padding: 7px 10px 7px 10px !important;
        line-height: 35px !important;
        height: 51px !important;
        min-width: 217px !important;
        text-decoration: none !important;
        display: inline-flex !important;
        color: #ffffff !important;
        background-color: #2ecc71 !important;
        border-radius: 5px !important;
        border: 1px solid transparent !important;
        padding: 7px 10px 7px 10px !important;
        font-size: 20px !important;
        letter-spacing: 0.6px !important;
        box-shadow: 0px 1px 2px rgba(190, 190, 190, 0.5) !important;
        -webkit-box-shadow: 0px 1px 2px 2px rgba(190, 190, 190, 0.5) !important;
        margin: 0 auto !important;
        -webkit-box-sizing: border-box !important;
        box-sizing: border-box !important;
        -o-transition: 0.3s all linear !important;
        -webkit-transition: 0.3s all linear !important;
        -moz-transition: 0.3s all linear !important;
        -ms-transition: 0.3s all linear !important;
        transition: 0.3s all linear !important;
    }

    .bmc-button:hover,
    .bmc-button:active,
    .bmc-button:focus {
        -webkit-box-shadow: 0px 1px 2px 2px rgba(190, 190, 190, 0.5) !important;
        text-decoration: none !important;
        box-shadow: 0px 1px 2px 2px rgba(190, 190, 190, 0.5) !important;
        opacity: 0.85 !important;
        color: #ffffff !important;
    }
</style>

<span>
    <a class="bmc-button" target="_blank" href="https://www.patreon.com/bePatron?u=37212546">
        <img src="data:image/png;base64,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
" alt="Become a Patron">
        <span style="margin-left:15px;font-size:19px !important;">
            Become a Patron
        </span>
    </a>
</span>
<p>Why?</p>
<ul>
<li>The articles are published on Patreon first.</li>
<li>There is a follow-up article that shares insights, used techniques, architecture, infrastructure.</li>
<li>You can get early access to projects in the Beta phase.</li>
<li>We can discuss your ideas, your doubts, and the current issues you face in your project.</li>
<li>You keep me going, it is a big boost to my motivation!</li>
</ul>
<p>I really appreciate a <strong>one time support</strong> too, you can do it via Buy Me A Coffee.</p>


<style>
    .bmc-button img {
        height: 34px !important;
        width: 35px !important;
        margin-bottom: 1px !important;
        box-shadow: none !important;
        border: none !important;
        vertical-align: middle !important;
    }

    .bmc-button {
        padding: 7px 10px 7px 10px !important;
        line-height: 35px !important;
        height: 51px !important;
        min-width: 217px !important;
        text-decoration: none !important;
        display: inline-flex !important;
        color: #ffffff !important;
        background-color: #2ecc71 !important;
        border-radius: 5px !important;
        border: 1px solid transparent !important;
        padding: 7px 10px 7px 10px !important;
        font-size: 20px !important;
        letter-spacing: 0.6px !important;
        box-shadow: 0px 1px 2px rgba(190, 190, 190, 0.5) !important;
        -webkit-box-shadow: 0px 1px 2px 2px rgba(190, 190, 190, 0.5) !important;
        margin: 0 auto !important;
        -webkit-box-sizing: border-box !important;
        box-sizing: border-box !important;
        -o-transition: 0.3s all linear !important;
        -webkit-transition: 0.3s all linear !important;
        -moz-transition: 0.3s all linear !important;
        -ms-transition: 0.3s all linear !important;
        transition: 0.3s all linear !important;
    }

    .bmc-button:hover,
    .bmc-button:active,
    .bmc-button:focus {
        -webkit-box-shadow: 0px 1px 2px 2px rgba(190, 190, 190, 0.5) !important;
        text-decoration: none !important;
        box-shadow: 0px 1px 2px 2px rgba(190, 190, 190, 0.5) !important;
        opacity: 0.85 !important;
        color: #ffffff !important;
    }
</style>

<span>
    <a class="bmc-button" target="_blank" href="https://www.buymeacoffee.com/tweakedtech">
        <img src="https://cdn.buymeacoffee.com/buttons/bmc-new-btn-logo.svg" alt="Buy me a coffee">
        <span style="margin-left:15px;font-size:19px !important;">
            Buy me a coffee
        </span>
    </a>
</span>
<p>Thanks for reading my story, I am grateful that you were here until the end.<br>
Stay tuned for the next one!</p>
]]></content>
        </item>
        
        <item>
            <title>How I built 6 (&#43;1) apps in one day</title>
            <link>https://danieldallos.com/posts/2020/07/how-i-built-6-1-apps-in-one-day/</link>
            <pubDate>Sun, 12 Jul 2020 22:12:18 +0200</pubDate>
            
            <guid>https://danieldallos.com/posts/2020/07/how-i-built-6-1-apps-in-one-day/</guid>
            <description>&amp;ldquo;One day, 7 apps, of course&amp;hellip; How long was that day? 168 hours? Define &amp;lsquo;one day&amp;rsquo;!&amp;quot;
Are you looking for the definition?
The Story Usually, every product story starts with a pain, a problem that needs to be solved. A problem that bothers you, annoys you, you face it frequently and probably you are not alone with it.
This case was not different either.
A few months ago we started to use a new HR system that has a lot of advantages compared to the old one.</description>
            <content type="html"><![CDATA[<p><em>&ldquo;One day, 7 apps, of course&hellip; How long was that day? 168 hours? Define &lsquo;one day&rsquo;!&quot;</em><br>
Are you looking for the <a href="#definition-of-one-day">definition</a>?</p>
<h2 id="the-story">The Story</h2>
<p>Usually, every product story starts with a pain, a problem that needs to be solved. A problem that bothers you, annoys you, you face it frequently and probably you are not alone with it.<br>
This case was not different either.</p>
<p>A few months ago we started to use a new HR system that has a lot of advantages compared to the old one. But it had disadvantages too&hellip; Missing functionality, missing information&hellip; or the information is there, but hard to get it out from the system.</p>
<p>That's when the idea of <a href="https://pandoo.tweaked.tech/">Pandoo</a> (the holiday planner) was born.</p>

<figure  class="center" 
     style="width:40%"
    >
    <a href="https://pandoo.tweaked.tech">
        <img src="/images/flutter-pandoo/pandoo_icon_original.png" alt="Pandoo Icon" />
    </a>
    
</figure>

<h2 id="the-problem">The Problem</h2>
<p>The new tool did lack some features that we got used to.</p>
<p>A nice(r) and clean(er) way to see all of your holidays, your team's holidays (and type of those holidays), and book them in a bunch.</p>
<p><strong>Disclaimer:</strong> <em>Maybe some of these problems are manageable with admin configurations or with educating the users.. but usually, when people switch tools they just expect that &ldquo;it works&rdquo; and they have all the things they had previously plus nice extras.</em></p>
<p><em>The goal of this article is not to blame the tool, it is about the solution and the key learnings.</em></p>
<h2 id="the-solution">The Solution</h2>
<p>I wanted to bring back the functionalities from the old tool. I wanted to see my holidays as I was used to, I wanted to book my holidays as I used to. And I was not alone with this.</p>
<h3 id="the-requirements">The Requirements</h3>
<ul>
<li>I don't want to waste too much time on the MVP, just get it to work with the basic functionality and make me happy.</li>
<li>There is no way that I will build multiple apps using multiple programming languages.</li>
<li>Mobile-first, but bigger screen support would be nice where I can see more data.</li>
<li>Challenge myself.</li>
</ul>
<h3 id="the-choice">The Choice</h3>
<h4 id="flutter">Flutter</h4>

<figure  class="center" 
    
    >
    
        <img src="/images/flutter-pandoo/flutter.png" alt="Flutter logo" />
    
    
</figure>

<p>Why?</p>
<ul>
<li>Flutter's promise</li>
</ul>
<p><a href="https://flutter.dev/">Flutter</a> is a cross-platform tool made by Google, that helps you <strong>build native mobile apps from a single codebase</strong>.<br>
<em>(Edit (19/09/2020): This has been changed a bit as you will see it later. )</em></p>
<ul>
<li>I had some experiences already.</li>
</ul>
<p>I have built <a href="http://edengreen.tweaked.tech/">Edengreen</a>, a tool that helps you check your available meal vouchers.<br>
It is a really basic app, but it taught me the basics of how Flutter works. At this time Flutter was still in beta.</p>
<p>One of my other projects was <a href="https://coronavirusalert.tweaked.tech/">CoronaVirus Alert</a>, a tool that monitors the infections country by country and sends you push notifications.<br>
In this project, I have played with Firebase, serverless functions, and custom scripts here and there. I have learned that there is a <strong>big pool of third-party libraries</strong> for Flutter and <strong>the community is growing rapidly</strong> too.</p>
<ul>
<li>Web support</li>
</ul>
<p>I have never tried, but apparently, there was rudimentary Web support too.</p>
<ul>
<li>The challenge</li>
</ul>
<p>My previous projects helped me to understand the basics but I wanted to achieve more and the Web support was sounded interesting too.</p>
<h3 id="know-your-stuff">Know Your Stuff</h3>
<p>I needed a giant scrollable table view where horizontally I can show the days of the year and vertically all of my colleagues and their holidays.<br>
This sounds easy, but in reality, it never is. Weird, annoying edge-cases when you are almost done, lack of Flutter knowledge from my side, unforeseen platform limitations, etc&hellip;</p>
<p>I wanted to see what is available already by the community, maybe there is something that is similar and it can fit my needs (with minimal tweaking). Luckily I have found the <a href="https://pub.dev/packages/horizontal_data_table">horizontal_data_table</a> package, that provided 90% of what I have needed&hellip; I have forked the package and did some fine-tuning for my needs on the way.</p>
<p>Getting the data from the HR tool was pretty easy, they provide a nice REST API to interact with the back-end.</p>
<p>When you connect the dots you have to be cautious. You need to know what you expect from your tool, how it is going to work. How does it get the data? Do we need to store the data locally? Do we need an extra server-side logic too? Do we plan to use any exotic UI elements?</p>
<p>These are important questions, because when you pick a library/package in Flutter and e.g. if it works on Android it does not mean automatically that it works on iOS too&hellip; or even on the Web. You need to pick libraries that rely on the default native support and no additional native third-party libraries. (Or the reliance on those libraries needs to be complete on every side.)</p>
<p>For example, if you would like to use Firebase services in your app, you need to check which platforms <a href="https://github.com/FirebaseExtended/flutterfire">are supported</a>. <strong>If your target is not, probably you will face a runtime crash when Flutter tries to access the non-existent APIs.</strong></p>
<p>When you search for a Flutter package in the official <a href="https://pub.dev/">Dart/Flutter package repository</a>, you can also see which platforms are supported.</p>

<figure  class="center" 
    
    >
    <a href="/images/flutter-pandoo/package_platform_support.png">
        <img src="/images/flutter-pandoo/package_platform_support.png" alt="Package Platform support" />
    </a>
    
</figure>

<h3 id="3-apps-at-once">3 Apps at Once</h3>
<p>Based on past experiences in the MVP I did not choose any special external library (except the horizontal_data_table) because I wanted to limit the compatibility issues between iOS, Android, and Web.</p>
<p>After I was done with the minimal version, I tested the implementation on Android and iOS.<br>
It worked and looked nice! (Sure, it had some performance issues here and there while scrolling through the holidays of the whole company&hellip; but on a limited dataset it was good.)</p>

<figure  class="center" 
     style="width:40%"
    >
    <a href="/images/flutter-pandoo/android.png">
        <img src="/images/flutter-pandoo/android.png" alt="Android support" />
    </a>
    
    <figcaption>
        <p style="text-align: center;">
        Pandoo on Android
        
            
        
        </p> 
    </figcaption>
    
</figure>


<figure  class="center" 
     style="width:45%"
    >
    <a href="/images/flutter-pandoo/ios.png">
        <img src="/images/flutter-pandoo/ios.png" alt="iOS support" />
    </a>
    
    <figcaption>
        <p style="text-align: center;">
        Pandoo on iOS
        
            
        
        </p> 
    </figcaption>
    
</figure>

<p>So now we have <strong>two apps</strong> ready to use!</p>
<p>Let's see how it works on the Web.<br>
Of course, it crashed in a few places 😀.
But nothing serious that a good old &ldquo;if-else&rdquo; could not solve. I have still used some APIs that were not available on the Web, so I needed to use <code>kIsWeb</code> <a href="https://api.flutter.dev/flutter/foundation/kIsWeb-constant.html">constant</a> to differ the logic between Web and native mobile.</p>
<p>It also had some UI quirks due to unsupported/buggy UI elements (<a href="https://github.com/flutter/flutter/issues/45505">with gradients</a>), but it was good enough for the current phase.</p>

<figure  class="center" 
     style="width:100%"
    >
    <a href="/images/flutter-pandoo/web_table.png">
        <img src="/images/flutter-pandoo/web_table.png" alt="Web support - table view" />
    </a>
    
    <figcaption>
        <p style="text-align: center;">
        Pandoo on Web with rendering issues (grey boxes, weird dividers)
        
            
        
        </p> 
    </figcaption>
    
</figure>


<figure  class="center" 
     style="width:100%"
    >
    <a href="/images/flutter-pandoo/web_balance.png">
        <img src="/images/flutter-pandoo/web_balance.png" alt="Web support - balance view" />
    </a>
    
    <figcaption>
        <p style="text-align: center;">
        Pandoo on Web showing the Holiday Balance
        
            
        
        </p> 
    </figcaption>
    
</figure>

<p>We have the <strong>third app</strong> too, and you could say <em>&ldquo;Ok, you are done, you have it all, Web runs on all platforms&rdquo;</em>.</p>
<p>But wait, there is more!</p>
<h3 id="the-meetup-and-the-extra-3-apps">The Meetup and The Extra 3 Apps</h3>
<p>I really like Meetups. I can meet with like-minded people, listen to good and interesting presentations, and sometimes <a href="https://www.meetup.com/gdg-brussels/photos/27573830/457831934/">I give a talk too</a> 😊.</p>
<p>There was a promising Meetup from <a href="https://www.meetup.com/gdg-brussels/events/268503398/">GDG Brussels on Flutter topic</a>. Based on the presentation summary, it intended to be a basic Flutter intro and the experiences of the author.<br>
So it could be nothing interesting for me, I am kinda done with the basics&hellip; but I always like to repeat similar things, because maybe I can discover a new bit of information that can change my perspective and start me thinking.
For the same reason, I read/listen to books on similar topics too&hellip; even the 95% is the same as a previous book, the remaining 5% is worth the time.</p>
<p><strong>Luckily this case was not different either.</strong><br>
The presenter walked through their app development challenges and how quickly they have re-implemented the apps from scratch in Flutter.</p>
<p>Then he showed something interesting. His app was in a weird-looking window. It looked like a browser or a bezel-less iOS simulator. Apparently, it was his application compiled into a  <strong>native MacOS app</strong>. It turned out that Flutter added support for <a href="https://flutter.dev/desktop">Desktop</a> platforms. Obviously, this was even in an earlier stage than the Web support, but it is worth a try.</p>
<p>The next day I tried it, it worked! I had Pandoo as a native MacOS app. Meanwhile, I have discovered that there is also really basic support for Linux and Windows&hellip; After the initial setup of the environments in VirtualBox, I could compile Pandoo as a <strong>native Windows and Linux app</strong> too! 🎉</p>
<p>Sure, there were some extra limitations on Windows and Linux, I had to do some extra &ldquo;if-else&rdquo; conditions. E.g. there was no persistent storage API available on these platforms, so I could not save any data to the disk.<br>
<em>(Edit (19/09/2020): Later I did polyfill these missing features by using the default Dart file reading and writing APIs.)</em></p>
<p>Extra <strong>three apps are done</strong>. Sounds good, doesn't it?</p>

<figure  class="center" 
     style="width:100%"
    >
    <a href="/images/flutter-pandoo/ubuntu.png">
        <img src="/images/flutter-pandoo/ubuntu.png" alt="Ubuntu support" />
    </a>
    
    <figcaption>
        <p style="text-align: center;">
        Pandoo on Ubuntu
        
            
        
        </p> 
    </figcaption>
    
</figure>


<figure  class="center" 
     style="width:100%"
    >
    <a href="/images/flutter-pandoo/windows.png">
        <img src="/images/flutter-pandoo/windows.png" alt="Windows support" />
    </a>
    
    <figcaption>
        <p style="text-align: center;">
        Pandoo on Windows
        
            
        
        </p> 
    </figcaption>
    
</figure>


<figure  class="center" 
     style="width:100%"
    >
    <a href="/images/flutter-pandoo/macos.png">
        <img src="/images/flutter-pandoo/macos.png" alt="MacOS support" />
    </a>
    
    <figcaption>
        <p style="text-align: center;">
        Pandoo on MacOS showing the Holiday Balance
        
            
        
        </p> 
    </figcaption>
    
</figure>

<h3 id="1">+1</h3>
<p>We can argue on this if it counts as the seventh app or not. But it was such a nice surprise to me, so please allow me to count it as +1.</p>
<p>While I was testing Pandoo in the Chrome Browser on my phone, I got a popup that asked me if I would like to add Pandoo to my home screen. Sure, why not. But this icon on my home screen looked different from another page link. And&hellip; it also appeared between my apps in the Launcher. What?</p>
<p>It turned out that the <strong>Web apps built by Flutter are PWA applications</strong> too. So even if the device is offline, you can open and use the web apps.
Or, you could&hellip; if a webpage is made for that. In my case I don't cache any data from the HR tool, I fetch all the data from the API every time.
That means when Pandoo is loaded inside the browser and you try to use it without an Internet connection, it shows an API error (just like all the other versions of the app). I could save the last fetched data locally and load it immediately at app start (while the new is loading)&hellip; but for now, it is ok as it is.</p>
<h2 id="conclusion">Conclusion</h2>
<p>I think Flutter is a strong candidate in the app development era. Not only for mobile but in general.</p>
<p>I did not write a single line of code to have a Mac OSX app. Although I have some experiences with Mac OSX apps (<a href="https://mike.tweaked.tech">Mike</a>) so I would have an idea where to start, it is not a copy/paste from an iOS app, you need to put some work there. Hopefully <a href="https://developer.apple.com/mac-catalyst/">Mac Catalyst</a> will help with this in the future.</p>
<p>Write an app for Windows and Linux? I would not even think about it.</p>
<p>The additional Web support is really nice, and it can be really useful. For example, if you have a Revolut and TransferWise account you probably noticed that the mobile apps are very similar to each other, and they also provide a Web version too with a limited feature set. I don't know what technologies they use, but (if not Flutter already) I can imagine in the future these kinds of services will consider Flutter for their apps.</p>
<h3 id="definition-of-one-day">Definition of &lsquo;One Day&rsquo;</h3>
<p>Probably at this point, you think <em>&ldquo;How on Earth was this possible in one day&rdquo;</em>?</p>
<p>My definition of one day: <strong>14-16 hours</strong>.</p>
<p>In this timeframe, I was able to develop the most important part of the app, the giant table view with all the data in it, the holiday balance view, and the way of requesting holidays one by one.<br>
I don't count the extra menus, extra functionalities, going to the Meetup 😀.<br>
For the additional basic Desktop support, I don't count any minute. Basically, there was no code change done, except when I saw a crash I did put an &ldquo;if-else&rdquo; around the problematic part and it was &ldquo;fixed&rdquo;. Also, the later polyfill changes were not taken into account.</p>
<h3 id="key-learnings">Key learnings</h3>
<p>In general:</p>
<ul>
<li>To build a prototype, Flutter is one of the best tools now on the market.</li>
<li>You don't have to over-engineer an MVP.</li>
<li>Better to know the limitations of your tool upfront than being surprised later.</li>
<li>Meetups are great to meet interesting people and gather new information.</li>
</ul>
<p>About Flutter:</p>
<ul>
<li>It supports Android, iOS, Web, Windows, MacOS, Linux (and <a href="https://en.wikipedia.org/wiki/Google_Fuchsia">Fuchsia</a>).</li>
<li>If a native third-party library exists, but there is no Flutter support yet, <a href="https://flutter.dev/docs/development/platform-integration/platform-channels">you can make it Flutter compatible</a>.</li>
<li>The core Dart libraries are available on all platforms, so in the worst case, you can fall-back on them.</li>
<li>The community is huge.</li>
</ul>
<h2 id="support">Support</h2>
<p>Did you enjoy my story? There is more in the pipeline&hellip; 😉</p>
<p>Do you want to know more insights? Would you like to discuss the used techniques, or would you like to see some part of the code?
Then consider becoming a <strong>monthly supporter</strong> on Patreon via my <strong>Tweaked.Tech</strong> initiative.</p>



<style>
    .bmc-button img {
        height: 30px !important;
        width: 30px !important;
        margin-bottom: 1px !important;
        box-shadow: none !important;
        border: none !important;
        vertical-align: middle !important;
    }

    .bmc-button {
        padding: 7px 10px 7px 10px !important;
        line-height: 35px !important;
        height: 51px !important;
        min-width: 217px !important;
        text-decoration: none !important;
        display: inline-flex !important;
        color: #ffffff !important;
        background-color: #2ecc71 !important;
        border-radius: 5px !important;
        border: 1px solid transparent !important;
        padding: 7px 10px 7px 10px !important;
        font-size: 20px !important;
        letter-spacing: 0.6px !important;
        box-shadow: 0px 1px 2px rgba(190, 190, 190, 0.5) !important;
        -webkit-box-shadow: 0px 1px 2px 2px rgba(190, 190, 190, 0.5) !important;
        margin: 0 auto !important;
        -webkit-box-sizing: border-box !important;
        box-sizing: border-box !important;
        -o-transition: 0.3s all linear !important;
        -webkit-transition: 0.3s all linear !important;
        -moz-transition: 0.3s all linear !important;
        -ms-transition: 0.3s all linear !important;
        transition: 0.3s all linear !important;
    }

    .bmc-button:hover,
    .bmc-button:active,
    .bmc-button:focus {
        -webkit-box-shadow: 0px 1px 2px 2px rgba(190, 190, 190, 0.5) !important;
        text-decoration: none !important;
        box-shadow: 0px 1px 2px 2px rgba(190, 190, 190, 0.5) !important;
        opacity: 0.85 !important;
        color: #ffffff !important;
    }
</style>

<span>
    <a class="bmc-button" target="_blank" href="https://www.patreon.com/bePatron?u=37212546">
        <img src="data:image/png;base64,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
" alt="Become a Patron">
        <span style="margin-left:15px;font-size:19px !important;">
            Become a Patron
        </span>
    </a>
</span>
<p>Why?</p>
<ul>
<li>The articles are published on Patreon first.</li>
<li>There is a follow-up article that shares insights, used techniques, architecture, infrastructure.</li>
<li>You can get early access to projects in the Beta phase.</li>
<li>We can discuss your ideas, your doubts, and the current issues you face in your project.</li>
<li>You keep me going, it is a big boost to my motivation!</li>
</ul>
<p>I really appreciate a <strong>one time support</strong> too, you can do it via Buy Me A Coffee.</p>


<style>
    .bmc-button img {
        height: 34px !important;
        width: 35px !important;
        margin-bottom: 1px !important;
        box-shadow: none !important;
        border: none !important;
        vertical-align: middle !important;
    }

    .bmc-button {
        padding: 7px 10px 7px 10px !important;
        line-height: 35px !important;
        height: 51px !important;
        min-width: 217px !important;
        text-decoration: none !important;
        display: inline-flex !important;
        color: #ffffff !important;
        background-color: #2ecc71 !important;
        border-radius: 5px !important;
        border: 1px solid transparent !important;
        padding: 7px 10px 7px 10px !important;
        font-size: 20px !important;
        letter-spacing: 0.6px !important;
        box-shadow: 0px 1px 2px rgba(190, 190, 190, 0.5) !important;
        -webkit-box-shadow: 0px 1px 2px 2px rgba(190, 190, 190, 0.5) !important;
        margin: 0 auto !important;
        -webkit-box-sizing: border-box !important;
        box-sizing: border-box !important;
        -o-transition: 0.3s all linear !important;
        -webkit-transition: 0.3s all linear !important;
        -moz-transition: 0.3s all linear !important;
        -ms-transition: 0.3s all linear !important;
        transition: 0.3s all linear !important;
    }

    .bmc-button:hover,
    .bmc-button:active,
    .bmc-button:focus {
        -webkit-box-shadow: 0px 1px 2px 2px rgba(190, 190, 190, 0.5) !important;
        text-decoration: none !important;
        box-shadow: 0px 1px 2px 2px rgba(190, 190, 190, 0.5) !important;
        opacity: 0.85 !important;
        color: #ffffff !important;
    }
</style>

<span>
    <a class="bmc-button" target="_blank" href="https://www.buymeacoffee.com/tweakedtech">
        <img src="https://cdn.buymeacoffee.com/buttons/bmc-new-btn-logo.svg" alt="Buy me a coffee">
        <span style="margin-left:15px;font-size:19px !important;">
            Buy me a coffee
        </span>
    </a>
</span>
<p>Thanks for reading my story, I am grateful that you were here until the end.<br>
Stay tuned for the next one!</p>
]]></content>
        </item>
        
        <item>
            <title>How to spy on your iOS users - a missing camera privacy feature</title>
            <link>https://danieldallos.com/posts/2019/12/how-to-spy-on-your-ios-users-by-using-the-camera/</link>
            <pubDate>Sat, 21 Dec 2019 22:12:18 +0100</pubDate>
            
            <guid>https://danieldallos.com/posts/2019/12/how-to-spy-on-your-ios-users-by-using-the-camera/</guid>
            <description>History I think everyone remembers the scandal (around November 2019) about an app in which the camera was activated in the &amp;ldquo;background&amp;rdquo; due to a bug. If you missed it, quickly Google it. The online media was full of these kinds of articles at that time.
Then my mind did not let me sleep&amp;hellip;
Camera Permission on iOS Description from Apple:
 In iOS, the user must explicitly grant permission for each app to access cameras and microphones.</description>
            <content type="html"><![CDATA[<h2 id="history">History</h2>
<p>I think everyone remembers the scandal (around November 2019) about an app in which the camera was activated in the &ldquo;background&rdquo; due to a bug.
If you missed it, quickly Google it. The online media was full of these kinds of articles at that time.</p>
<p>Then my mind did not let me sleep&hellip;</p>
<h2 id="camera-permission-on-ios">Camera Permission on iOS</h2>
<p><a href="https://developer.apple.com/documentation/avfoundation/cameras_and_media_capture/requesting_authorization_for_media_capture_on_ios?language=objc">Description</a> from Apple:</p>
<blockquote>
<p>In iOS, the user must explicitly grant permission for each app to access cameras and microphones. Before your app can use the capture system for the first time, iOS shows an alert asking the user to grant your app access to the camera, as shown below. <strong>iOS remembers the user’s response to this alert, so subsequent uses of the capture system don’t cause it to appear again.</strong> The user can change permission settings for your app in Settings &gt; Privacy.</p>
</blockquote>
<h2 id="camera-api">Camera API</h2>
<p><a href="https://developer.apple.com/documentation/avfoundation/cameras_and_media_capture/setting_up_a_capture_session">The Camera API usage</a> is pretty straightforward. You define which camera hardware you would like to use, how to show a preview, and where to save the files.</p>
<h2 id="the-problem">The Problem</h2>
<p>As you see from the documentation above, once the user grants the Camera Permission for an app, <strong>it is granted. Forever.</strong></p>
<p>Anyone can make an application that asks for Camera Permission. e.g. taking a photo, uploading an image, setting profile picture, &ldquo;scanning&rdquo; document, etc. You can find many use-cases that can justify camera usage.
The application can ask it for a valid reason, but after that, you don't know what can happen.</p>
<p>And do you know if the app is still using your camera while you are in the app? Probably not.</p>
<p>Let your imagination fly a bit and think about what you could do if you could have access to the user's camera anytime in the app.</p>
<blockquote>
<p>You have a home decor online shop. While the user is searching for a certain item in your app you could look around in his/her house, see the colors, furniture, and your app could start suggesting extra items in matching style.</p>
</blockquote>
<p>Or:</p>
<blockquote>
<p>Your main revenue stream is displaying ads in your app. What if you could monitor the user's reaction to an ad? Then you could offer to your advertiser more insights about what kind of ads a certain type of user likes.</p>
</blockquote>
<p>This information would be pretty valuable, right? Ethical? I don't think so.</p>
<h2 id="proof-of-concept">Proof of Concept</h2>
<h3 id="the-good">The Good</h3>
<p><strong>Note:</strong> If you would like to go straight to the code, here you are:  <a href="https://github.com/Danesz/PrivacyGuard-iOS/tree/master/HiddenCam">HiddenCamera</a> app on GitHub.</p>
<p>As a first step, you have to define the <a href="https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html#//apple_ref/doc/plist/info/NSCameraUsageDescription">NSCameraUsageDescription</a> key Info.plist file of your application. You have to define a purpose string for this key why your app needs this permission. Obviously, taking pictures&hellip; right?</p>

    <figure class="left" >
        <img src="/images/how-to-spy-ios/info_plist.png"   />

        
    </figure>


<p>This description will be part of the alert message that will be shown to the users the first time when they use your app.</p>

    <figure class="left" >
        <img src="/images/how-to-spy-ios/asking_persmission.png"   />

        
    </figure>


<p>So, let's take a look at our <a href="https://github.com/Danesz/PrivacyGuard-iOS/blob/master/HiddenCam/HiddenCam/CamerVIewController.swift">CameraViewController</a>.</p>
<p>We need to initialize an <code>AVCaptureSession</code>.</p>
<pre><code class="language-swift">let session = AVCaptureSession()
</code></pre>
<p>This session is the base of everything. Here we can attach our input/output elements.</p>
<pre><code class="language-swift">var deviceInput: AVCaptureDeviceInput!

session.sessionPreset = AVCaptureSession.Preset.vga640x480

// acquire the camera device
guard let device = AVCaptureDevice
    .default(AVCaptureDevice.DeviceType.builtInWideAngleCamera,
             for: .video,
             position: AVCaptureDevice.Position.front) else {
                return
}

do {
    deviceInput = try AVCaptureDeviceInput(device: device)
    guard deviceInput != nil else {
        print(&quot;error: can't get deviceInput&quot;)
        return
    }
    
    // add the Input device to the session
    if self.session.canAddInput(deviceInput){
        self.session.addInput(deviceInput)
    }
    
    // define a Video output device to capture video frames from the camera
    videoDataOutput = AVCaptureVideoDataOutput()
    videoDataOutput.alwaysDiscardsLateVideoFrames = true
    videoDataOutputQueue = DispatchQueue(label: &quot;VideoDataOutputQueue&quot;)
    videoDataOutput.setSampleBufferDelegate(self, queue:self.videoDataOutputQueue)
    
    if session.canAddOutput(self.videoDataOutput){
        session.addOutput(self.videoDataOutput)
    }
    
    //define a Picture output device to capture still images from the camera
    stillImageOutput = AVCapturePhotoOutput()
    
    if session.canAddOutput(stillImageOutput) {
        session.addOutput(self.stillImageOutput)
    }
    
    videoDataOutput.connection(with: .video)?.isEnabled = true
    
    // define the camera preview view
    previewLayer = AVCaptureVideoPreviewLayer(session: self.session)
    previewLayer.videoGravity = AVLayerVideoGravity.resizeAspect
    
  	// ...
    
    // and start running the capture session
    session.startRunning()
} catch let error as NSError {
    deviceInput = nil
    print(&quot;error: \(error.localizedDescription)&quot;)
}
</code></pre>
<p>The <code>startRunning()</code> method kicks in the whole capturing process. (This will be important later!)</p>
<p>Now we can listen to the video delegate callbacks:</p>
<pre><code class="language-swift">func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
    // do stuff here with the video
}
</code></pre>
<p>Then to the photo delegate callbacks:</p>
<pre><code class="language-swift">func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
    //process single image
    guard let imageData = photo.fileDataRepresentation()
        else { return }
    
    let captureImageView = UIImageView(frame: CGRect(x: 50, y: 50, width: 100, height: 100))
    let image = UIImage(data: imageData)
    captureImageView.image = image
    
    //show the captured image
    let centerView = UIView(frame: CGRect(x: UIScreen.main.bounds.size.width / 2 - 100,
                                          y: UIScreen.main.bounds.size.height / 2 - 100,
                                          width: 200,
                                          height: 200))
    centerView.backgroundColor = UIColor.red
    centerView.addSubview(captureImageView)
    
    self.view.addSubview(centerView)
    
}
</code></pre>
<p>As you can see, nothing evil.
We ask for permission, the user grants it, we show the video preview and the captured photo.</p>
<p>Let's see how it works:</p>

<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
  <iframe src="https://www.youtube.com/embed/-BI70Twu_QI" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" allowfullscreen title="YouTube Video"></iframe>
</div>

<h3 id="the-bad">The Bad</h3>
<p>We can take a look on the <a href="https://github.com/Danesz/PrivacyGuard-iOS/blob/master/HiddenCam/HiddenCam/CamerVIewControllerHidden.swift">CameraViewControllerHidden</a> class.
Same thing as above, but unfortunately by mistake we forgot to attach our video preview to the view.</p>
<pre><code class="language-swift">// ...
do {
    deviceInput = try AVCaptureDeviceInput(device: captureDevice)
    guard deviceInput != nil else {
        print(&quot;error: can't get deviceInput&quot;)
        return
    }
    
    if self.session.canAddInput(deviceInput){
        self.session.addInput(deviceInput)
    }
    
    stillImageOutput = AVCapturePhotoOutput()
    
    if session.canAddOutput(stillImageOutput) {
        session.addOutput(self.stillImageOutput)
    }
    
    // we can also attach videoDataOutput from the previous example to get all the video frames
    // but for now image capturing is enough

    // and whoopsie, we forgot the preview :(
    
    session.startRunning()
}
// ...
</code></pre>
<p>What do we see now?</p>
<p>No permission was asked (previously it was granted forever), no video preview.
So we think everything is alright, we are browsing our feed, watching ads, liking/disliking photos&hellip; meanwhile someone is spying on us:</p>

<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
  <iframe src="https://www.youtube.com/embed/noPKjEO8MDI" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" allowfullscreen title="YouTube Video"></iframe>
</div>

<p>Just to give you a little peace of mind: at least the camera stream is blocked while the app is in the background.</p>
<h2 id="jailbreak-to-the-rescue">Jailbreak to the Rescue</h2>
<p>What is <a href="https://en.wikipedia.org/wiki/IOS_jailbreaking">Jailbreaking</a>?</p>
<blockquote>
<p>Jailbreaking is the privilege escalation of an Apple device for the purpose of removing software restrictions imposed by Apple on iOS, iPadOS, tvOS and watchOS operating systems. This is typically done by using a series of kernel patches. Jailbreaking permits root access in Apple's mobile operating system, allowing the installation of software that is unavailable through the official Apple App Store. Many types of jailbreaking are available, for different versions.</p>
</blockquote>
<p><a href="https://support.apple.com/en-gb/HT201954">Apple publicly disapproves of jailbreaking.</a></p>
<p>There are a lot of pros and cons of jailbreaking. You can find some of these on the above-mentioned links.</p>
<p>Jailbreaking is for users who know what they are doing. And these people like to jailbreak for tweaking their devices.
Just take a look at <a href="https://www.idownloadblog.com/tag/jailbreak-apps-tweaks/">all the magic</a> you can do on your iOS device once it is jailbroken.</p>
<h3 id="the-tweak">The Tweak</h3>
<p>The <a href="https://en.wikipedia.org/wiki/IOS_jailbreaking#Device_customization">tweaks</a> are iOS applications, however</p>
<blockquote>
<p>&hellip;many of them are not typical self-contained apps but instead are extensions and customization options for iOS and its features and other apps (commonly called tweaks). Users install these programs for purposes including personalization and customization of the interface by tweaks developed by developers and designers, adding desired features and fixing annoyances&hellip;</p>
</blockquote>
<h3 id="privacyguard---ios">PrivacyGuard - iOS</h3>
<p><strong>Note: This post is not elaborating how to develop a Jailbreak tweak. That's for a future post. If you want to hear more about it, please subscribe to the <a href="https://subscribe.danieldallos.com">content notifications</a>.</strong></p>
<p>As you read above, most of the Jailbreak tweaks are extending the functionality of already existing apps. How? In a nutshell, they hook into existing methods in the app and react when the method is called. They can completely override it, alter its behavior, or just trigger a jailbreak code and call the original method after it.</p>
<p>This is exactly what we will do.</p>
<p><strong>Note:</strong> If you would like to go straight to the code, here you are:  <a href="https://github.com/Danesz/PrivacyGuard-iOS/tree/master/privacyguard">PrivacyGuard-iOS</a> tweak on GitHub.</p>
<p>As we saw above in the camera app examples, everything starts with the <code>startRunning()</code> method on <code>AVCaptureSession</code>.</p>
<p>It's time to <a href="https://github.com/Danesz/PrivacyGuard-iOS/blob/master/privacyguard/Tweak.x">hook</a>!</p>
<pre><code class="language-objectivec">#import &lt;UIKit/UIKit.h&gt;

%hook SpringBoard
-(void) applicationDidFinishLaunching:(id)arg {
	%orig(arg);
	UIAlertView *lookWhatWorks = [[UIAlertView alloc] initWithTitle:@&quot;PrivacyGuard Tweak&quot;
		message:@&quot;Your privacy guard is running 😎&quot;
		delegate:self
		cancelButtonTitle:@&quot;OK&quot;
		otherButtonTitles:nil];
	[lookWhatWorks show];
}
%end

%hook AVCaptureSession

// Hooking an instance method with no arguments.
-(void) startRunning {
	NSString *appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@&quot;CFBundleIdentifier&quot;];
	NSLog(@&quot;PrivacyGuard startRunning: %@&quot;, appName);

	UIView *statusBar = [[UIApplication sharedApplication] valueForKey:@&quot;statusBar&quot;];
	statusBar.backgroundColor = [UIColor greenColor];

	%orig;

}

-(void) stopRunning {
	NSLog(@&quot;PrivacyGuard stopRunning&quot;);

	UIView *statusBar = [[UIApplication sharedApplication] valueForKey:@&quot;statusBar&quot;];
	statusBar.backgroundColor = [UIColor clearColor]; // or we could save and restore the original one

	%orig;
}

// Always make sure you clean up after yourself; Not doing so could have grave consequences!
%end
</code></pre>
<p>Easy (for now) and beautiful!</p>
<p>(The first part, hooking into <code>Springboard</code>, just a helper notification to show the tweak is initialized.)</p>
<p>We hook directly into <code>AVCaptureSession</code>'s <code>startRunning()</code> method. Whenever this method is called we log to the console the current app that is using the camera (initiated the capturing) and change the status bar color to green to visually notify ourselves.
After this is done we just call the original (<code>%orig</code>) method and let the flow continue.
Hooking into the <code>stopRunning()</code> method is useful to revert our changes after the camera is not in use anymore.</p>
<p>How does it look while using the example apps above?</p>
<p>When you have the preview visible:

<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
  <iframe src="https://www.youtube.com/embed/VjPIccppKzI" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" allowfullscreen title="YouTube Video"></iframe>
</div>
</p>
<p>And when we are in evil mode:

<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
  <iframe src="https://www.youtube.com/embed/QdRo74cjcPU" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" allowfullscreen title="YouTube Video"></iframe>
</div>
</p>
<p>Also, in the logs we can see the relevant app info:

    <figure class="left" >
        <img src="/images/how-to-spy-ios/privacyguard_log.png"   />

        
    </figure>

</p>
<p>And no hidden surprises anymore.</p>
<p>You can go further. Showing a popup, notification, or a little icon in the status bar while the camera is in use. It is all up to you!</p>
<p><strong>Note:</strong> PrivacyGuard-iOS is not available in any Jailbreak repository (yet). If you have the urge to try it, please let me know. But also feel free to compile it and tweak it for yourself.</p>
<p><strong>Note:</strong> If there are other ways of accessing the camera stream those were not part of this investigation. Probably there you can use the same approach.</p>
<h2 id="conclusions">Conclusions</h2>
<h3 id="jailbreak-tweaks">Jailbreak Tweaks</h3>
<p>The tweaks made by the Jailbreak community are amazing. Lot of smart ideas, solutions for daily annoyances.</p>
<p>Having the possibility to use a jailbroken device and develop your own tweaks for your development work can be really interesting.
It opens your possibilities, you have more access to the system, you can debug more things, private APIs.</p>
<p>It is brilliant.</p>
<h3 id="camera-api-1">Camera API</h3>
<p>I think you can draw your own conclusion.</p>
<p>Be aware of your apps and used services.
Maybe you don't even need those apps, just try to use the web version of those services. Check the granted permissions of your apps, maybe for your use-case it does not need extra permission.</p>
<p>Sensitive permissions should not be granted forever.
Hopefully, Apple will fix this issue soon like Google is doing it on <a href="https://developer.android.com/preview/privacy/permissions">Android 11</a> now.</p>
<h2 id="disclaimer">Disclaimer</h2>
<p>I don't say that any of these things are happening in any apps on the market and I don't encourage you to jailbreak your iOS device.</p>
<p>But better to be prepared and be more privacy-conscious. Having these kinds of data collected about a user/content/ad could be valuable for certain parties.</p>
<h2 id="support">Support</h2>
<p>Did you enjoy my story? There is more in the pipeline&hellip; 😉</p>
<p>Do you want to know more insights? Would you like to discuss the used techniques, or would you like to see some part of the code?
Then consider becoming a <strong>monthly supporter</strong> on Patreon via my <strong>Tweaked.Tech</strong> initiative.</p>



<style>
    .bmc-button img {
        height: 30px !important;
        width: 30px !important;
        margin-bottom: 1px !important;
        box-shadow: none !important;
        border: none !important;
        vertical-align: middle !important;
    }

    .bmc-button {
        padding: 7px 10px 7px 10px !important;
        line-height: 35px !important;
        height: 51px !important;
        min-width: 217px !important;
        text-decoration: none !important;
        display: inline-flex !important;
        color: #ffffff !important;
        background-color: #2ecc71 !important;
        border-radius: 5px !important;
        border: 1px solid transparent !important;
        padding: 7px 10px 7px 10px !important;
        font-size: 20px !important;
        letter-spacing: 0.6px !important;
        box-shadow: 0px 1px 2px rgba(190, 190, 190, 0.5) !important;
        -webkit-box-shadow: 0px 1px 2px 2px rgba(190, 190, 190, 0.5) !important;
        margin: 0 auto !important;
        -webkit-box-sizing: border-box !important;
        box-sizing: border-box !important;
        -o-transition: 0.3s all linear !important;
        -webkit-transition: 0.3s all linear !important;
        -moz-transition: 0.3s all linear !important;
        -ms-transition: 0.3s all linear !important;
        transition: 0.3s all linear !important;
    }

    .bmc-button:hover,
    .bmc-button:active,
    .bmc-button:focus {
        -webkit-box-shadow: 0px 1px 2px 2px rgba(190, 190, 190, 0.5) !important;
        text-decoration: none !important;
        box-shadow: 0px 1px 2px 2px rgba(190, 190, 190, 0.5) !important;
        opacity: 0.85 !important;
        color: #ffffff !important;
    }
</style>

<span>
    <a class="bmc-button" target="_blank" href="https://www.patreon.com/bePatron?u=37212546">
        <img src="data:image/png;base64,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
" alt="Become a Patron">
        <span style="margin-left:15px;font-size:19px !important;">
            Become a Patron
        </span>
    </a>
</span>
<p>Why?</p>
<ul>
<li>The articles are published on Patreon first.</li>
<li>There is a follow-up article that shares insights, used techniques, architecture, infrastructure.</li>
<li>You can get early access to projects in the Beta phase.</li>
<li>We can discuss your ideas, your doubts, and the current issues you face in your project.</li>
<li>You keep me going, it is a big boost to my motivation!</li>
</ul>
<p>I really appreciate a <strong>one time support</strong> too, you can do it via Buy Me A Coffee.</p>


<style>
    .bmc-button img {
        height: 34px !important;
        width: 35px !important;
        margin-bottom: 1px !important;
        box-shadow: none !important;
        border: none !important;
        vertical-align: middle !important;
    }

    .bmc-button {
        padding: 7px 10px 7px 10px !important;
        line-height: 35px !important;
        height: 51px !important;
        min-width: 217px !important;
        text-decoration: none !important;
        display: inline-flex !important;
        color: #ffffff !important;
        background-color: #2ecc71 !important;
        border-radius: 5px !important;
        border: 1px solid transparent !important;
        padding: 7px 10px 7px 10px !important;
        font-size: 20px !important;
        letter-spacing: 0.6px !important;
        box-shadow: 0px 1px 2px rgba(190, 190, 190, 0.5) !important;
        -webkit-box-shadow: 0px 1px 2px 2px rgba(190, 190, 190, 0.5) !important;
        margin: 0 auto !important;
        -webkit-box-sizing: border-box !important;
        box-sizing: border-box !important;
        -o-transition: 0.3s all linear !important;
        -webkit-transition: 0.3s all linear !important;
        -moz-transition: 0.3s all linear !important;
        -ms-transition: 0.3s all linear !important;
        transition: 0.3s all linear !important;
    }

    .bmc-button:hover,
    .bmc-button:active,
    .bmc-button:focus {
        -webkit-box-shadow: 0px 1px 2px 2px rgba(190, 190, 190, 0.5) !important;
        text-decoration: none !important;
        box-shadow: 0px 1px 2px 2px rgba(190, 190, 190, 0.5) !important;
        opacity: 0.85 !important;
        color: #ffffff !important;
    }
</style>

<span>
    <a class="bmc-button" target="_blank" href="https://www.buymeacoffee.com/tweakedtech">
        <img src="https://cdn.buymeacoffee.com/buttons/bmc-new-btn-logo.svg" alt="Buy me a coffee">
        <span style="margin-left:15px;font-size:19px !important;">
            Buy me a coffee
        </span>
    </a>
</span>
<p>Thanks for reading my story, I am grateful that you were here until the end.<br>
Stay tuned for the next one!</p>
]]></content>
        </item>
        
    </channel>
</rss>
