Pages

Thursday, 19 October 2017

Lock BlackBerry KEYone With Password When Using Fingerprint

BlackBerry KEYone, an advancement from BlackBerry Passport, which however uses Android, an OS not so great when compared to QNX, but still fine given the fact that it can be hardened and there are apps that makes business get done. Getting required apps on BlackBerry 10 is really a pain, no matter how great the device and OS is. Plus developers like me can program KEYone using any of the supported JVM languages instead of banging my head against C/C++, a pain for any functional programmer accustomed to living in the JVM world.

So, now to the point. BlackBerry KEYone has fingerprint sensor on the spacebar. However, we know fingerprint is a security issue. When confiscated, we are legally required to unlock the phone if the finger is still on the hand. But, that is not the case with passwords. There are a lot of defences against providing authorities with your password. So KEYone can lock the device which will then require you to enter the password even if it is configured to unlock using fingerprint. For that, long press the key K, which opens BlackBerry Launcher and initially it asks whether you need to enable the app to administer the device. Allow access and the device will be locked using that keyboard shortcut, which then requires the password to unlock. Rest of the time, unlock using fingerprint. Makes life easier without compromising security. BlackBerry rocks!

Tuesday, 10 October 2017

jamf equals no privacy

jamf is used for managing Apple devices in the enterprise. However it is a nasty little piece of software. It always sends online, offline status to enterprise JSS endpoint. Few excerpts from /var/log/jamf.log.
Tue Oct 03 15:26:03 purgatory jamf[52]: Daemon starting
Tue Oct 03 15:26:05 purgatory jamf[363]: 
There was an error.

     Connection failure: "The Internet connection appears to be offline."

Tue Oct 03 15:26:06 purgatory jamf[52]: Network state changed, checking for policies...
Tue Oct 03 15:26:06 purgatory jamf[407]: Could not connect to the JSS. Looking for cached policies...
Tue Oct 03 15:26:07 purgatory jamf[52]: Network state changed, checking for policies...
Tue Oct 03 15:26:10 purgatory jamf[52]: Network state changed, checking for policies...
Tue Oct 03 15:26:13 purgatory jamf[52]: Network state changed, checking for policies...
Tue Oct 03 15:26:29 purgatory jamf[52]: Informing the JSS about login for user castiel
Tue Oct 03 15:26:38 purgatory jamf[52]: Network state changed, checking for policies...
Tue Oct 03 15:27:12 purgatory jamf[52]: Network state changed, checking for policies...
Tue Oct 03 15:27:17 purgatory jamf[52]: Network state changed, checking for policies...
Tue Oct 03 15:27:38 purgatory jamf[52]: Network state changed, checking for policies...
Tue Oct 03 15:27:56 purgatory jamf[52]: Network state changed, checking for policies...
Tue Oct 03 15:28:03 purgatory jamf[52]: Network state changed, checking for policies...
Tue Oct 03 15:28:04 purgatory jamf[2573]: Checking for policies triggered by "networkStateChange" for user "castiel"...
Tue Oct 03 15:28:04 purgatory jamf[2356]: Checking for policies triggered by "networkStateChange" for user "castiel"...
Tue Oct 03 15:28:04 purgatory jamf[2087]: Checking for policies triggered by "networkStateChange" for user "castiel"...
Tue Oct 03 15:28:07 purgatory jamf[2573]: Could not connect to the JSS. Looking for cached policies...
Tue Oct 03 15:28:07 purgatory jamf[2087]: Could not connect to the JSS. Looking for cached policies...
***
Wed Oct 04 20:02:06 purgatory jamf[13815]: Checking for policies triggered by "recurring check-in" for user "castiel"...
Wed Oct 04 20:02:10 purgatory jamf[13815]: Could not connect to the JSS. Looking for cached policies...
Wed Oct 04 20:05:33 purgatory jamf[52]: Network state changed, checking for policies...
***
Thu Oct 05 09:26:25 purgatory jamf[99672]: Checking for policies triggered by "networkStateChange"...
Thu Oct 05 10:04:40 purgatory jamf[52]: Informing the JSS about login for user root
***
Sat Oct 07 07:51:30 purgatory jamf[64843]: Checking for policies triggered by "networkStateChange" for user "castiel"...
Sat Oct 07 08:02:52 purgatory jamf[69647]: Checking for policies triggered by "recurring check-in" for user "castiel"...
Sat Oct 07 08:02:55 purgatory jamf[69647]: Executing Policy Enable local firewall
Sat Oct 07 08:02:56 purgatory jamf[69647]: Executing Policy Inventory Daily
Sat Oct 07 08:02:57 purgatory jamf[69647]: Executing Policy Update Username Field in Inventory
Sat Oct 07 08:29:25 purgatory jamf[52]: Network state changed, checking for policies...
Sat Oct 07 08:30:27 purgatory jamf[87635]: Checking for policies triggered by "networkStateChange" for user "castiel"...
Sat Oct 07 08:30:28 purgatory jamf[87262]: Checking for policies triggered by "recurring check-in" for user "castiel"...
Sat Oct 07 08:31:45 purgatory jamf[87635]: Could not connect to the JSS. Looking for cached policies...
Sat Oct 07 08:31:46 purgatory jamf[87262]: Could not connect to the JSS. Looking for cached policies...
Sat Oct 07 08:31:46 purgatory jamf[87262]: Executing Offline Policy Enable local firewall
Sat Oct 07 08:49:57 purgatory jamf[97458]: Checking for policies triggered by "recurring check-in" for user "castiel"...
Sat Oct 07 08:51:15 purgatory jamf[97458]: Could not connect to the JSS. Looking for cached policies...
Sat Oct 07 08:51:15 purgatory jamf[97458]: Executing Offline Policy Enable local firewall
Sat Oct 07 09:09:24 purgatory jamf[8836]: Checking for policies triggered by "recurring check-in" for user "castiel"...
Sat Oct 07 09:10:41 purgatory jamf[8836]: Could not connect to the JSS. Looking for cached policies...
Sat Oct 07 09:10:41 purgatory jamf[8836]: Executing Offline Policy Enable local firewall
***
Mon Oct 09 10:27:46 purgatory jamf[20026]: Checking for policies triggered by "recurring check-in" for user "castiel"...
Mon Oct 09 13:45:26 purgatory jamf[28409]: Checking for policies triggered by "recurring check-in" for user "castiel"...
Mon Oct 09 13:45:29 purgatory jamf[28409]: Executing Policy Inventory Daily
Mon Oct 09 14:16:56 purgatory jamf[28409]: Error running recon: Connection failure: "The request timed out."
Mon Oct 09 14:20:26 purgatory jamf[52]: Daemon shutdown completed
Mon Oct 09 14:20:26 purgatory jamf[52]: Daemon exiting
So every time the jamf infected computer goes online or offline, changed to root etc., the Sauron will be notified about it. More interesting part comes next.

It tracks all applications used by the users and the amount of time spend with it. That treasure is in /Library/Application Support/JAMF/Usage folder. There will be folders like 2017-10-07, 2017-10-08, 2017-10-09. Looking at one of those folders will give logs like (null).plist, idle.plist, castiel.plist, etc.

Let us see what castiel.plist has to offer.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>/Applications/Calendar.app</key>
    <dict>
        <key>foremost</key>
        <string>0</string>
        <key>open</key>
        <string>968</string>
        <key>secondsforemost</key>
        <string>0</string>
        <key>secondsopen</key>
        <string>58112</string>
        <key>version</key>
        <string>9.0</string>
    </dict>
    <key>/Applications/Cisco/Cisco AnyConnect Secure Mobility Client.app</key>
    <dict>
        <key>foremost</key>
        <string>1</string>
        <key>open</key>
        <string>1911</string>
        <key>secondsforemost</key>
        <string>55</string>
        <key>secondsopen</key>
        <string>114686</string>
        <key>version</key>
        <string>4.1.08005</string>
    </dict>
    <key>/Applications/GIMP.app</key>
    <dict>
        <key>foremost</key>
        <string>0</string>
        <key>open</key>
        <string>1911</string>
        <key>secondsforemost</key>
        <string>0</string>
        <key>secondsopen</key>
        <string>114685</string>
        <key>version</key>
        <string>2.8.18</string>
    </dict>
    <key>/Applications/GitHub Desktop.app</key>
    <dict>
        <key>foremost</key>
        <string>0</string>
        <key>open</key>
        <string>1911</string>
        <key>secondsforemost</key>
        <string>0</string>
        <key>secondsopen</key>
        <string>114685</string>
        <key>version</key>
        <string>Hasty Things Done Hastily</string>
    </dict>
    <key>/Applications/Google Chrome.app</key>
    <dict>
        <key>foremost</key>
        <string>0</string>
        <key>open</key>
        <string>1911</string>
        <key>secondsforemost</key>
        <string>0</string>
        <key>secondsopen</key>
        <string>114685</string>
        <key>version</key>
        <string>63.0.3223.8</string>
    </dict>
    <key>/Applications/Mail.app</key>
    <dict>
        <key>foremost</key>
        <string>2</string>
        <key>open</key>
        <string>1911</string>
        <key>secondsforemost</key>
        <string>134</string>
        <key>secondsopen</key>
        <string>114686</string>
        <key>version</key>
        <string>10.3</string>
    </dict>
    <key>/Applications/Notes.app</key>
    <dict>
        <key>foremost</key>
        <string>1</string>
        <key>open</key>
        <string>1911</string>
        <key>secondsforemost</key>
        <string>27</string>
        <key>secondsopen</key>
        <string>114686</string>
        <key>version</key>
        <string>4.4</string>
    </dict>
    <key>/Applications/Photos.app</key>
    <dict>
        <key>foremost</key>
        <string>0</string>
        <key>open</key>
        <string>1911</string>
        <key>secondsforemost</key>
        <string>0</string>
        <key>secondsopen</key>
        <string>114685</string>
        <key>version</key>
        <string>2.0</string>
    </dict>
    <key>/Applications/Postman.app</key>
    <dict>
        <key>foremost</key>
        <string>0</string>
        <key>open</key>
        <string>1911</string>
        <key>secondsforemost</key>
        <string>0</string>
        <key>secondsopen</key>
        <string>114685</string>
        <key>version</key>
        <string>5.2.1</string>
    </dict>
    <key>/Applications/Preview.app</key>
    <dict>
        <key>foremost</key>
        <string>0</string>
        <key>open</key>
        <string>1911</string>
        <key>secondsforemost</key>
        <string>0</string>
        <key>secondsopen</key>
        <string>114686</string>
        <key>version</key>
        <string>9.0</string>
    </dict>
    <key>/Applications/Reminders.app</key>
    <dict>
        <key>foremost</key>
        <string>1</string>
        <key>open</key>
        <string>54</string>
        <key>secondsforemost</key>
        <string>31</string>
        <key>secondsopen</key>
        <string>3272</string>
        <key>version</key>
        <string>4.0</string>
    </dict>
    <key>/Applications/Safari Technology Preview.app</key>
    <dict>
        <key>foremost</key>
        <string>65</string>
        <key>open</key>
        <string>1911</string>
        <key>secondsforemost</key>
        <string>3920</string>
        <key>secondsopen</key>
        <string>114685</string>
        <key>version</key>
        <string>11.1</string>
    </dict>
    <key>/Applications/Slack.app</key>
    <dict>
        <key>foremost</key>
        <string>56</string>
        <key>open</key>
        <string>1892</string>
        <key>secondsforemost</key>
        <string>3381</string>
        <key>secondsopen</key>
        <string>113558</string>
        <key>version</key>
        <string>2.8.1</string>
    </dict>
    <key>/Applications/Sublime Text.app</key>
    <dict>
        <key>foremost</key>
        <string>33</string>
        <key>open</key>
        <string>1662</string>
        <key>secondsforemost</key>
        <string>2026</string>
        <key>secondsopen</key>
        <string>99735</string>
        <key>version</key>
        <string>Build 3143</string>
    </dict>
    <key>/Applications/Utilities/Keychain Access.app</key>
    <dict>
        <key>foremost</key>
        <string>0</string>
        <key>open</key>
        <string>1911</string>
        <key>secondsforemost</key>
        <string>0</string>
        <key>secondsopen</key>
        <string>114685</string>
        <key>version</key>
        <string>9.0</string>
    </dict>
    <key>/Applications/Utilities/Terminal.app</key>
    <dict>
        <key>foremost</key>
        <string>1</string>
        <key>open</key>
        <string>1908</string>
        <key>secondsforemost</key>
        <string>9</string>
        <key>secondsopen</key>
        <string>114527</string>
        <key>version</key>
        <string>2.7.3</string>
    </dict>
    <key>/Applications/VLC.app</key>
    <dict>
        <key>foremost</key>
        <string>0</string>
        <key>open</key>
        <string>1911</string>
        <key>secondsforemost</key>
        <string>0</string>
        <key>secondsopen</key>
        <string>114686</string>
        <key>version</key>
        <string>2.2.6</string>
    </dict>
    <key>/Applications/[...snip..]Crypt.app</key>
    <dict>
        <key>foremost</key>
        <string>0</string>
        <key>open</key>
        <string>1911</string>
        <key>secondsforemost</key>
        <string>0</string>
        <key>secondsopen</key>
        <string>114685</string>
        <key>version</key>
        <string>1.xx</string>
    </dict>
    <key>/Applications/Xcode.app</key>
    <dict>
        <key>foremost</key>
        <string>0</string>
        <key>open</key>
        <string>1911</string>
        <key>secondsforemost</key>
        <string>0</string>
        <key>secondsopen</key>
        <string>114686</string>
        <key>version</key>
        <string>9.0</string>
    </dict>
    <key>/Applications/iTunes.app</key>
    <dict>
        <key>foremost</key>
        <string>0</string>
        <key>open</key>
        <string>1911</string>
        <key>secondsforemost</key>
        <string>0</string>
        <key>secondsopen</key>
        <string>114686</string>
        <key>version</key>
        <string>12.7</string>
    </dict>
    <key>/System/Library/CoreServices/CoreServicesUIAgent.app</key>
    <dict>
        <key>foremost</key>
        <string>0</string>
        <key>open</key>
        <string>1911</string>
        <key>secondsforemost</key>
        <string>0</string>
        <key>secondsopen</key>
        <string>114685</string>
        <key>version</key>
        <string>168.3</string>
    </dict>
    <key>/System/Library/CoreServices/Finder.app</key>
    <dict>
        <key>foremost</key>
        <string>1</string>
        <key>open</key>
        <string>1911</string>
        <key>secondsforemost</key>
        <string>55</string>
        <key>secondsopen</key>
        <string>114685</string>
        <key>version</key>
        <string>10.12.5</string>
    </dict>
    <key>/System/Library/CoreServices/SystemUIServer.app</key>
    <dict>
        <key>foremost</key>
        <string>0</string>
        <key>open</key>
        <string>1911</string>
        <key>secondsforemost</key>
        <string>0</string>
        <key>secondsopen</key>
        <string>114686</string>
        <key>version</key>
        <string>1.7</string>
    </dict>
    <key>/System/Library/CoreServices/UserNotificationCenter.app</key>
    <dict>
        <key>foremost</key>
        <string>0</string>
        <key>open</key>
        <string>1911</string>
        <key>secondsforemost</key>
        <string>0</string>
        <key>secondsopen</key>
        <string>114686</string>
        <key>version</key>
        <string>3.3.0</string>
    </dict>
    <key>/System/Library/CoreServices/loginwindow.app</key>
    <dict>
        <key>foremost</key>
        <string>1685</string>
        <key>open</key>
        <string>1911</string>
        <key>secondsforemost</key>
        <string>101144</string>
        <key>secondsopen</key>
        <string>114686</string>
        <key>version</key>
        <string>9.0</string>
    </dict>
    <key>/System/Library/Frameworks/ScreenSaver.framework/Resources/ScreenSaverEngine.app</key>
    <dict>
        <key>foremost</key>
        <string>0</string>
        <key>open</key>
        <string>1911</string>
        <key>secondsforemost</key>
        <string>0</string>
        <key>secondsopen</key>
        <string>114686</string>
        <key>version</key>
        <string>5.0</string>
    </dict>
    <key>/System/Library/Frameworks/Security.framework/Versions/A/MachServices/SecurityAgent.bundle</key>
    <dict>
        <key>foremost</key>
        <string>0</string>
        <key>open</key>
        <string>1911</string>
        <key>secondsforemost</key>
        <string>0</string>
        <key>secondsopen</key>
        <string>114686</string>
        <key>version</key>
        <string>9.0</string>
    </dict>
    <key>/System/Library/Frameworks/WebKit.framework/Versions/A/XPCServices/com.apple.WebKit.WebContent.xpc</key>
    <dict>
        <key>foremost</key>
        <string>0</string>
        <key>open</key>
        <string>1911</string>
        <key>secondsforemost</key>
        <string>0</string>
        <key>secondsopen</key>
        <string>114686</string>
        <key>version</key>
        <string>12603</string>
    </dict>
    <key>/private/var/folders/9m/_fh0czw947g8q7pdhxbfbjdh0000gn/T/AppTranslocation/AB999B43-95BD-4B9E-880D-6C59DFD81558/d/Base64.app</key>
    <dict>
        <key>foremost</key>
        <string>1</string>
        <key>open</key>
        <string>72</string>
        <key>secondsforemost</key>
        <string>7</string>
        <key>secondsopen</key>
        <string>4344</string>
        <key>version</key>
        <string>1.0</string>
    </dict>
</dict>
</plist>
Oops, now the pointy honchos knows which applications I am using and for how long in a day. New age micro-management.


The cure
The cure is very simple however. Turn off the jamf service. And next time when we want to turn it on, say to see if IT has pushed some clever software, clear all the logs before hand, close all apps. Then load the daemon back.
sudo launchctl load /Library/LaunchDaemons/com.jamfsoftware.jamf.daemon.plist 
sudo launchctl load /Library/LaunchDaemons/com.jamfsoftware.task.1.plist #com.jamfsoftware.task.{n}.plist check the folder for correct number
Let the update get pushed to the system, then turn it off.
Also as per policy, we cannot set the OS X firewall in stealth mode, blocking all connections. It automatically changes to "on" mode as the policy will be forced down the throat.

Turn off jamf
sudo launchctl unload /Library/LaunchDaemons/com.jamfsoftware.jamf.daemon.plist 
sudo launchctl unload /Library/LaunchDaemons/com.jamfsoftware.task.1.plist #com.jamfsoftware.task.{n}.plist check the folder for correct number
RIP jamf.

Also jamf can do screen sharing, with or without user consent if configured so. It is a RAT as well.

Monday, 4 September 2017

[RFC 4226] HOTP for BlackBerry 10 (QNX 6)

Implemented RFC 4226 - HOTP: An HMAC based One-Time-Password algorithm for BlackBerry 10 running QNX 6. Uses BlackBerry Cryptographic Kernel version 5.6 (SB-GSE-56). Various things are pending and the app in the current state can be found at github called QAuthenticator. A reference implementation of the algorithm in Groovy is at hotp.groovy.

I must say, BlackBerry cryptography library is just fantastic. And Qt makes C++ fun to program after me programming in higher level languages like Clojure.

Sunday, 3 September 2017

Linking Libraries in a BlackBerry 10 Cascades Project

Usually when developing C/C++ project we specify linked libraries in a Makefile. With BlackBerry 10 Cascades project, the libraries can be added in the <ProjectName>.pro file. This file is less clouded than the Makefile and easily manageable. For example, if you are developing a crypto app, you need to link the crypto library which is huapi. Add the below to the .pro file.
LIBS += -lhuapi
Else we will get errors like undefined reference to symbol and error adding symbols: DSO missing from command line.
Now we are ready to conquer the world!

Monday, 21 August 2017

Getting Started with Cordova and ClojureScript

This is a small writeup about getting started quickly with Cordova and ClojureScript.

1. Install cordova as usual.
$ sudo npm install -g cordova
2. Create an app.
$ cordova create MyApp
3. Add platform.
$ cd MyApp
$ cordova platform add ios
Cordova app is all set. Now instead of JavaScript we want to use ClojureScript. This part can be treated as a separate project. Write ClojureScript, compile it to JavaScript, place it in the www folder of the cordova project and build the project as usual with appropriate overrides in the respective platform folders.

Install leiningen, Java 8, Clojure 1.8.
1. Create a lein project. (We are inside MyApp folder).
$ lein new my-app
2. Update the project.clj as shown.
(defproject myapp "0.1.0"
  :description "My Lovely App"
  :url "http://example.com"
  :plugins [[lein-cljsbuild "1.1.7"]]
  :dependencies [[org.clojure/clojure "1.8.0"]
                 [org.clojure/clojurescript "1.9.521"]]
  :cljsbuild {
    :builds [{
      :id "core"
      :source-paths ["src"]
      :compiler {
        :output-to "../www/js/myapp.js"
        :optimizations :whitespace
        :pretty-print true}}]})
The source folder is the src in the my-app directory. Now we are ready to write some code.
3. The main file inside src is com/example/myapp/core.cljs
(ns com.example.myapp.core)

(defn foo []
  (println "Hello, World!"))  ; Note we use println rather than console.log

(enable-console-print!)  ; this translates the println to console.log
(set! js/foo foo)  ; export the function declared in this module (namespace) to global
3. Compile the ClojureScript to JavaScript. This will watch for modifications and auto compile every time.
lein cljsbuild auto
4. Include the my-app.js in the index.html file and load it in the browser. Open console and type foo(), which will print Hello, World! to the console.

Now we are in the right path to building lovely apps with ClojureScript and Apache Cordova! But there is a caveat. This hello world app generate 32281 lines of code. I do not see much advantage in using ClojureScript as I am a big believer of being a minimalist when doing front-end development and pretty comfortable with all the craziness that JavaScript offers. Plus this has to run on smartphones and consume less energy but I don't have the luxury of going fully native. So my take, pure JavaScript.

Thursday, 17 August 2017

NPE in getUnmarshallerFactory() in OpenSAML 3

If you are getting NullPointerException when trying to get unmarshaller factory in OpenSAML 3, means most likely OpenSAML 3 has not been initialized. I was calling OpenSAML 3 methods in my unit test suite which gave me the below error. Call (InitializationService/initialize) to initialize the library. My main program does the initialization, so I do not get error when running, but unit test does not invoke that code path.
lein test :only com.concur.saml.saml-test/saml-response

ERROR in (saml-response) (XMLObjectProviderRegistrySupport.java:126)
Uncaught exception, not in assertion.
expected: nil
  actual: java.lang.NullPointerException: null
 at org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport.getUnmarshallerFactory (XMLObjectProviderRegistrySupport.java:126)
    com.concur.saml.core$get_response.invokeStatic (core.clj:253)
    com.concur.saml.core$get_response.invoke (core.clj:249)
    com.concur.saml.saml_test$fn__7679.invokeStatic (saml_test.clj:22)
    com.concur.saml.saml_test/fn (saml_test.clj:19)
 ...
    user$eval85$fn__136.invoke (form-init5596096413821124374.clj:1)
    clojure.lang.AFn.applyToHelper (AFn.java:156)
    clojure.lang.AFn.applyTo (AFn.java:144)
    clojure.core$apply.invokeStatic (core.clj:648)
...
    clojure.lang.AFn.applyToHelper (AFn.java:156)
    clojure.lang.Var.applyTo (Var.java:700)
    clojure.main.main (main.java:37)

Ran 1 tests containing 1 assertions.
0 failures, 1 errors.
Tests failed.

Wednesday, 31 May 2017

Slide - SAML, Variants, Functors, Monads and Exceptions

A presentation I gave at work (SAP Concur) on SAML, using variants and exception handling in Clojure, functors, applicative functors and monads in Haskell and how Maybe and Either monads short circuits during exception and such. Removed some internal code and links.


The variant C code can be downloaded from Github.
Download this slide from Github.

Friday, 12 May 2017

BlackBerry Passport MicroSDXC Card Support

BlackBerry Passport supports microSD cards upto 128GB. microSDXC cards can also be used with it. However, BlackBerry 10 recognises only FAT formatted external partitions and these cards comes mostly with ExFAT. So the device will show that the media card is not supported and is downloading drivers, but it will fail with an error. To fix this, erase the card and choose FAT as the partition format. Then the OS will recognise the microSDXC card.

Monday, 8 May 2017

Get RSA PublicKey from XML Key Format

Here is a script (prototype) in Groovy to get RSA PublicKey from XML public key. You might encounter such XML keys, say during .NET interop.
import javax.xml.parsers.DocumentBuilder
import javax.xml.parsers.DocumentBuilderFactory
import org.w3c.dom.Document
import java.nio.charset.StandardCharsets
import java.security.spec.RSAPublicKeySpec
import java.security.KeyFactory
import java.security.PublicKey

def rsaPubXML = "ANxn+vSe8nIdRSy0gHkGoJQnUIIJ3WfOV7hsSk9An9LRafuZXYUMB6H5RxtWFm72f7nPKlg2N5kpqk+oEuhPx4IrnXIqnN5vwu4Sbc/w8rjE3XxcGsgXUams3wgiBJ0r1/lLCd6a61xRGtj4+Vae+Ps3mz/TdGUkDf80dVek9b9VAQAB"
def docBuilderFactory = DocumentBuilderFactory.newInstance()
def docBuilder = docBuilderFactory.newDocumentBuilder()

def b64Decode(enc) {
    Base64.getDecoder().decode(enc)
}

Document xmlDoc = docBuilder.parse(new ByteArrayInputStream(rsaPubXML.getBytes(StandardCharsets.UTF_8)))

def modulus = xmlDoc.getElementsByTagName("Modulus").item(0).textContent
def exponent = xmlDoc.getElementsByTagName("Exponent").item(0).textContent
println "modulus: ${modulus}\nexponent: ${exponent}"

RSAPublicKeySpec keySpec = new RSAPublicKeySpec(new BigInteger(b64Decode(modulus)), new BigInteger(b64Decode(exponent)));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey key = keyFactory.generatePublic(keySpec);

println "key: ${key}"
This gives the following output.
modulus: ANxn+vSe8nIdRSy0gHkGoJQnUIIJ3WfOV7hsSk9An9LRafuZXYUMB6H5RxtWFm72f7nPKlg2N5kpqk+oEuhPx4IrnXIqnN5vwu4Sbc/w8rjE3XxcGsgXUams3wgiBJ0r1/lLCd6a61xRGtj4+Vae+Ps3mz/TdGUkDf80dVek9b9V
exponent: AQAB
key: Sun RSA public key, 1024 bits
  modulus: 154774478177095248394968828543369801032226937226535865231262824893513573019304152154974259955740337204606655133945162319470662684517274530901497375379716962851415879364453962123395223899051919634994929603613704222239797911292193776910691509004328773391280872757318122152217457361921195935350223751896771182421
  public exponent: 65537
Note that the modulus must be a positive integer. If you are working with other JVM languages and are getting a negative integer value, specify the signum as 1 in the BigInteger(1, b64Decode(modulus)) function call. The exponent must always be 65537 as of now because that is the largest Fermat's Prime known today.

Sunday, 30 April 2017

Base64 macOS App Release

Released v1.0 of Base64 macOS app. It is a simple app for encoding and decoding base64 texts. It will encode texts as we type or paste. This program does not access any files or network and works offline. It is sandboxed as well.

Encode screen

Decode screen

Source code at GitHub. For downloads, check the release folder.

Saturday, 29 April 2017

Working with AppKit Delegates

Delegates are responders that acts to events that occurs in a program. AppKit delegates often work with Cocoa UI events. Here we will see two examples of handling events, one for NSTextField and another for NSTextView in conjunction with Interface Builder, rather than programatically.

1. Create a macOS Cocoa project from Xcode which will generate an AppDelegate and a ViewController as usual.
2. We will make the ViewController as the delegate to respond to events. For that we need to declare that the ViewController adopts the formal protocol defined by the delegates.
@interface ViewController : NSViewController<NSTextViewDelegate, NSTextFieldDelegate> {
3. Choose the Main.storyboard and choose the View Controller Scene, drag and drop Text View and Text Field components.
4. Choose the Text View from the Document Outline of the View Controller Scene, option click, and in the popup, connect the delegate outlet to the View Controller. Same for Text Field.

5. Now, in the ViewController.h header, declare two IBOutlets which will connect the components in the storyboard to the code.
@interface ViewController : NSViewController<NSTextViewDelegate, NSTextFieldDelegate> {
    IBOutlet NSTextView *textView;
    IBOutlet NSTextField *textField;
}
Since these interface builder outlets are not connected yet, the radio box is in unchecked state.
6. Go back to the interface builder (the storyboard file), choose Text View, option click, drag and connect the New Referencing Outlet to View Controller which brings the above IBOutlets. Choose textView to make the connection. Do the same for Text Field, but here we should choose textField as the referencing outlet.


7. Back to code, open ViewController.m implementation file and implement any of the delegated methods.
#pragma mark - delegates

/* NSTextView */
- (void)textDidChange:(NSNotification *)notification {
    NSLog(@"text did change");
    textView = [notification object];
    NSLog(@"string: %@", [textView string]);
}

/* NSTextField */
- (void)controlTextDidChange:(NSNotification *)obj {
    NSLog(@"control text did changed");
    textField = [obj object];
    NSLog(@"text: %@", [textField stringValue]);
}
The above methods are invoked when the text in a text view or text field changes. The same concept extends to Cocoa Touch and iOS development.

The sample project can be downloaded from github.

Wednesday, 26 April 2017

Call blocking in BlackBerry 10 for a known number

We do not need fancy app to block calls. By default, BB10 has option to block all incoming calls or none. Not individually. But there is a better way. Ideally, we should not be blocking calls, because the caller can identify that a call has been blocked or not. It will ring once and then get busy or some other tones. Better way is to just disable all notifications for a number. For that, first save the annoying number to your contacts and in the "Ringtones and Notifications" option for that contact, choose "Phone Calls" and turn off "All Notifications". That is all there is. Now the call gets received, but you would not know unless you look at the phone. No disturbance.

Monday, 17 April 2017

Slack for BlackBerry 10

There is no official Slack app for BlackBerry 10. Also, if we open a slack channel on the native BlackBerry 10 browser, it just takes to the team settings page. I have figured ways to make it work though, which in fact is quite simple. Quick way is to enable desktop mode in the BB10 browser and then channels load instead of seeing the settings page for the team. But it's not at all user friendly and unsupported browser message will be shown.

Another way is to install the Slack app for Android by sideloading the apk file. This works fine when the team uses email for login. But, when it's configured for SSO only, then we have to follow additional steps. The native browser can open apps that have deep linking registered. Slack does not seem to have this. In order to make this work, we need to use a browser which also runs on the Android runtime. So get the Aurora Browser installed, open the Slack app, choose SSO sign in and this time choose Aurora browser when the dialog pops up. After authentication, a page will be shown with a Download Slack app or Open in Slack app buttons. Choose the later and the Slack app launches, and you get logged in to the team. Enjoy all the awesomeness of the Android Slack app.

I have not checked whether notifications will get received. But I doubt that. So if you don't receive it, you can choose the send email notifications when away for five minutes option from the team settings. Though there is a slight delay in you getting notified, the problem is solved.

I have tried S10 app from BlackBerry World, but for some reasons, I don't get any notifications. Plus the app is rather minimal with text only interface. Kind of have an IRC feel to it.

2FA with SAP Authenticator for BlackBerry 10

The search for a decent 2FA app for BlackBerry 10 is over. SAP Authenticator works perfectly on BlackBerry 10 devices. This is a generic 2FA app and is not tied to any service like Duo Mobile for BlackBerry 10. As in Android, this app requires Barcode Scanner to be installed as well. Instead of Google Authenticator you can use this. It can be used for any 2FA that uses TOTP (Time-based OTP). Happy camper!

Tip: When a website provides options for 2FA app, choose Android as the platform which will display the QR code. Choosing BlackBerry will give the secret key which we have to enter in manually in the SAP Authenticator app for setup rather than scanning a QR code.

Sunday, 16 April 2017

youtube-dl for Muxing Streams

Let's say we need to download videos from youtube with the highest audio and video quality. Sometimes, the combination is not available with most downloaders. But youtube-dl can download separate audio and video streams and mux them together. Here are some commands to do that.
# List all available streams for a video
➜ youtube-dl -F "https://www.youtube.com/watch?v=abcd1234"
Outputs
[youtube] abcd1234: Downloading webpage
[youtube] abcd1234: Downloading video info webpage
[youtube] abcd1234: Extracting video information
[youtube] abcd1234: Downloading js player en_US-vfl5-0t5t
[youtube] abcd1234: Downloading js player en_US-vfl5-0t5t
[info] Available formats for abcd1234:
format code  extension  resolution note
249          webm       audio only DASH audio   56k , opus @ 50k, 1.52MiB
250          webm       audio only DASH audio   84k , opus @ 70k, 2.10MiB
171          webm       audio only DASH audio  126k , vorbis@128k, 3.36MiB
140          m4a        audio only DASH audio  127k , m4a_dash container, mp4a.40.2@128k, 3.69MiB
251          webm       audio only DASH audio  161k , opus @160k, 4.03MiB
278          webm       256x144    144p  101k , webm container, vp9, 13fps, video only, 2.41MiB
160          mp4        256x144    144p  112k , avc1.4d400c, 25fps, video only, 3.19MiB
242          webm       426x240    240p  140k , vp9, 25fps, video only, 2.34MiB
243          webm       640x360    360p  234k , vp9, 25fps, video only, 3.98MiB
133          mp4        426x240    240p  248k , avc1.4d4015, 25fps, video only, 7.13MiB
134          mp4        640x360    360p  254k , avc1.4d401e, 25fps, video only, 6.49MiB
244          webm       854x480    480p  355k , vp9, 25fps, video only, 6.22MiB
135          mp4        854x480    480p  559k , avc1.4d401e, 25fps, video only, 13.74MiB
247          webm       1280x720   720p  601k , vp9, 25fps, video only, 11.70MiB
136          mp4        1280x720   720p 1171k , avc1.4d401f, 25fps, video only, 28.41MiB
248          webm       1920x1080  1080p 1220k , vp9, 25fps, video only, 23.32MiB
137          mp4        1920x1080  1080p 2258k , avc1.640028, 25fps, video only, 57.07MiB
17           3gp        176x144    small , mp4v.20.3, mp4a.40.2@ 24k
36           3gp        320x180    small , mp4v.20.3, mp4a.40.2
43           webm       640x360    medium , vp8.0, vorbis@128k
18           mp4        640x360    medium , avc1.42001E, mp4a.40.2@ 96k
22           mp4        1280x720   hd720 , avc1.64001F, mp4a.40.2@192k (best)
➜
We can see that the highest quality video is 137 with 1080p 2258k and audio is 251 with 161k. But these are video only and audio only streams. Let's combine them.
➜ youtube-dl -f 137+251 "https://www.youtube.com/watch?v=abcd1234"
It will pick a compatible format when merging. If mp4 doesn't work it gets converted into mkv mostly.

Misc
Automatically choose best video and audio: -f bestvideo+bestaudio
Download subtitle if the video has one already uploaded: --write-srt --sub-lang en

Install BlackBerry Blend/Link under macOS Sierra

The installer Install BlackBerry 10 Desktop Software.app inside the BlackBerry 10 Desktop Software_1.2.0.58_B60.dmg image for BlackBerry Link/Blend does not start the installer under macOS Sierra. Running the binary inside the installer .app directly gives the following error:
➜  ~ /Volumes/BlackBerry\ 10\ Desktop\ Software/Install\ BlackBerry\ 10\ Desktop\ Software.app/Contents/MacOS/Install\ BlackBerry\ 10\ Desktop\ Software ; exit;
/Library/LaunchAgents/com.rim.BBLaunchAgent.plist: Could not find specified service
/Library/LaunchDaemons/com.rim.BBDaemon.plist: Could not find specified service
However under the Resources folder of the package content there is BlackBerry Blend.pkg installer. Running it brings up the installer window and the installation succeeds and everything works fine ever after.

Sunday, 2 April 2017

Fun with Hopper on OS X

It has be a while that I touched any assembly code. So I thought I will refresh, and have some fun while at it. Hopper is a disassembler for macOS. It has call flow graphs and pseudo code as in IDA Pro. Another awesome thing is the themes, which adds a modern touch, and is easy to use. Btw., I don't really use IDA Pro. This is more about using Hopper for disassemble and patching Mac OS X binaries. For that I will choose a real app. There was a case where I had to downgrade iTunes once. For that I had to use AppZapper. AppZapper is a paid software. When you open it you can see the nag screen. But removing the nag is simple.



Open the app in Hopper. The AppZapper package has only one binary file AppZapper. Proceed with the default choices, i.e., for the package the loader is FAT archive x86-64 bit and for the executable it is Mach-O 64 bits. It shows the entry point procedure at address 0000000100000f60. Before proceeding, enable "Show the HEX column" to easily see the hex of the instruction set, like in OllyDbg.

Now from the left symbols panel, under Proc. choose -[AZAppController applicationDidFinishLaunching:] the idx of which is 88. Alternatively you can directly go to the address of the proc at 000000010000a250. Choose the pseudo code mode from the toolbar to get an idea of what is happening.



What it does is that it loads the AZRegistrationWindowController and calls the validateExistingRegistrationInformation method whose return value is in rax register. Then it takes the lower bytes of the ax register and performs a bitwise and (the test instruction). If the result is 0, means al is 0, then the zero flag (ZF) is set. If zero flag is not set, then do a local jump to the address at loc_10000a6ac which is at 000000010000a6ac. What we need to do is to take that branch which will then skip loading the AZRegistrationWindowController window. To do that we need to change the jne instruction to je. So the easy way is to click on the jne line and click the hex mode, which will highlight the instruction. The hex for jne/jnz instruction is 85 and hex for je/jz is 84. So double click 85 in instruction 0F 85 FC 02 00 00 and replace it with 84. Go back to asm mode and you can see the updated instruction. Only that it is shown in bytes (db).



Now we need to export the binary. From File menu choose "Produce new executable" and save as AppZapper. Go to applications, control click AppZapper and choose "Show Package Contents" and navigate to MacOS. Now replace the executable with the patched one. If you want to preserve the original then, rename that to something else and copy the patched one as AppZapper. Open the app, and we are no more greeted with the nag screen!

But hey, we still got to register and have only 5 zaps, so we will have to purchase if we need to continue using :). The register option is available from the menu.

Thursday, 9 March 2017

Cancel future With Interrupt Check in Clojure

A snippet for future-cancel with interrupt check. Call this instead of calling future directly.
(defmacro int-future
  [& body]
  `(future (when (not (Thread/interrupted)) ~@body)))

Sunday, 26 February 2017

LinkedIn Has No Proper Privacy Control for Media

If your LinkedIn profile was public before and then you disabled your public profile, your profile picture is still accessible without having to login to LinkedIn. It is not just a caching issue as it is still available from CDN for months after the profile is being made private. The images are loaded from media.licdn.com and will be indexed by Google. Even if you delete your profile picture, the previous one is still accessible from the CDN. When I contacted LinkedIn support for content removal, the person told that media.licdn.com is not associated with LinkedIn and I will have to remove it myself and from the search engine which indexed it. The funny thing being that the domain certificate shows LinkedIn Corporation as the organisation. The whois also has LinkedIn in it. 😜
Update: I had it removed from Google search using Webmasters already. LinkedIn removed the image. Yay!

Wednesday, 18 January 2017

PGP on BlackBerry 10

In BlackBerry 10, there is an option for importing PGP keys under Security & Privacy. But to import the secret keys exported by GnuPG, you need to append the public key in the exported private key armored file. Then PGP will import fine. As per the documentation, you will be able to encrypt, decrypt from hub if you have active sync or Lotus Notes. So for personal keys with IMAP account, a great alternative is to sideload OpenKeychain android app. Import your exported .asc private key. To decrpyt a received email, copy the content, tap share and choose decrypt. Similarly, you can encrypt and share. Unfortunately there is no direct hub integration. Quick UI tip for Passport, swipe down from top and tap Size. Choose Zoomed out Full Screen option (middle one) for better experience. Most android apps looks great with this option.

Tuesday, 17 January 2017

Updating Gmail Password on BlackBerry 10

To my surprise, I am not able to update the gmail account password on my BlackBerry 10 device. Figured that if you choose the default setup where the hub does all the configuration, there won't be any options to edit the server settings or passwords later on. So if you intend to change passwords later on, then you should choose the advanced option and choose IMAP account. Key in the details manually, and you will be able to update the password if required. And for the current setup, you will have to delete the account from the device and add it again using the above method.

Saturday, 7 January 2017

EBNF Grammer for Parsing Chrome Bookmarks

The bookmarks html exported by Chrome is not a valid html. It has different rules with a different DTD. Here is an ANTLR 4 grammar for parsing the bookmarks with support for unicode characters in bookmark names.
grammar Bookmarks;
 
document : prolog? misc* meta* misc* dl misc*;

prolog : DTD;

misc 
    : COMMENT 
    | S
    ;

meta 
    : '<' TEXT '>' TEXT '</' TEXT '>'
    | '<' TEXT attribute* '>'
    ;

dl : '<' TEXT '><' TEXT '>' misc* dt* misc* '</' TEXT '><' TEXT '>';

dt 
    : '<' TEXT '><' tag attribute* '>' content '</' tag '>' 
    | '<' TEXT '><' tag attribute* '></' tag '>'
    | dl
    ;

attribute 
    : attributeName '=' attributeValue 
    | S
    ;

tag 
    : H3 
    | TEXT
    ;

attributeName : TEXT;

attributeValue : VAL;

content : TEXT+;

DTD : '<!'.*?'>';

COMMENT : '<!--' .*? '-->' S;

H3 : 'H3';

VAL : '"'.*?'"';

TEXT : [A-Za-z0-9:\/\.@\-_;\s*]+ | NameChar+;

fragment
NameChar
    : NameStartChar
    | '0'..'9'
    | '_'
    | '\u00B7'
    | '\u0300'..'\u036F'
    | '\u203F'..'\u2040'
    ;

fragment
NameStartChar
    : 'A'..'Z' | 'a'..'z'
    | '\u00C0'..'\u00D6'
    | '\u00D8'..'\u00F6'
    | '\u00F8'..'\u02FF'
    | '\u0370'..'\u037D'
    | '\u037F'..'\u1FFF'
    | '\u200C'..'\u200D'
    | '\u2070'..'\u218F'
    | '\u2C00'..'\u2FEF'
    | '\u3001'..'\uD7FF'
    | '\uF900'..'\uFDCF'
    | '\uFDF0'..'\uFFFD'
    ;

S : [ \t\r\n]+ -> skip;

The exported bookmarks sample.
<!DOCTYPE NETSCAPE-Bookmark-file-1>
<!-- This is an automatically generated file.
     It will be read and overwritten.
     DO NOT EDIT! -->
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
<TITLE>Bookmarks</TITLE>
<H1>Bookmarks</H1>
<DL><p>
    <DT><H3 ADD_DATE="1481473849" LAST_MODIFIED="1481473992" PERSONAL_TOOLBAR_FOLDER="true">Bookmarks bar</H3>
    <DL><p>
        <DT><H3 ADD_DATE="1481473866" LAST_MODIFIED="1481473967">Test 1</H3>
        <DL><p>
            <DT><A HREF="https://encrypted.google.com/" ADD_DATE="1481473884" ICON="">Google</A>
            <DT><A HREF="https://yandex.ru/" ADD_DATE="1481473892" ICON="">Яндекс</A>
            <DT><A HREF="http://example.com/" ADD_DATE="1481473954">Example Domain</A>
        </DL><p>
        <DT><H3 ADD_DATE="1481473872" LAST_MODIFIED="1481473980">Test 2</H3>
        <DL><p>
            <DT><A HREF="https://duckduckgo.com/" ADD_DATE="1481473902" ICON="">DuckDuckGo</A>
            <DT><A HREF="https://clojure.news/" ADD_DATE="1481473936" ICON="">Clojure News</A>
            <DT><A HREF="http://example.com/" ADD_DATE="1481473955">Example Domain</A>
        </DL><p>
        <DT><A HREF="https://yandex.ru/" ADD_DATE="1481473893" ICON="">Яндекс</A>
        <DT><A HREF="http://www.echojs.com/" ADD_DATE="1481473986" ICON=""></A>
        <DT><A HREF="https://clojure.news/" ADD_DATE="1481473992" ICON=""></A>
        <DT><H3 ADD_DATE="1481474004" LAST_MODIFIED="1481477692">Test 3</H3>
        <DL><p>
            <DT><A HREF="https://encrypted.google.com/" ADD_DATE="1481474004" ICON="">Google</A>
            <DT><A HREF="https://duckduckgo.com/" ADD_DATE="1481474004" ICON="">DuckDuckGo</A>
            <DT><A HREF="https://clojure.news/" ADD_DATE="1481474004" ICON="">Clojure News</A>
            <DT><H3 ADD_DATE="1481477681" LAST_MODIFIED="1481477681">Test 4</H3>
            <DL><p>
                <DT><A HREF="https://clojure.news/" ADD_DATE="1481477681" ICON="">Clojure News</A>
                <DT><A HREF="https://news.ycombinator.com/" ADD_DATE="1481477681" ICON="">Hacker News</A>
                <DT><A HREF="http://example.com/" ADD_DATE="1481477681">Example Domain</A>
            </DL><p>
            <DT><A HREF="https://news.ycombinator.com/" ADD_DATE="1481474004" ICON="">Hacker News</A>
            <DT><A HREF="http://example.com/" ADD_DATE="1481474004">Example Domain</A>
        </DL><p>
    </DL><p>
</DL><p>
clj-antlr library can be used to get the parse tree out of the grammer. Snippet to get the parse tree below. Use compiled version of the grammar for better performance.
(def bm (antlr/parser "/home/kadaj/dev/clojure/bookmarks-parser/grammar/Bookmarks.g4"))
(pprint (bm (slurp "/home/kadaj/dev/clojure/bookmarks-parser/resources/bookmarks.html")))
Which produces the following parse tree.
(:document
 (:prolog "")
 (:misc
  "\n")
 (:meta
  "<"
  "META"
  (:attribute
   (:attributeName "HTTP-EQUIV")
   "="
   (:attributeValue "\"Content-Type\""))
  (:attribute
   (:attributeName "CONTENT")
   "="
   (:attributeValue "\"text/html; charset=UTF-8\""))
  ">")
 (:meta "<" "TITLE" ">" "Bookmarks" "")
 (:meta "<" "H1" ">" "Bookmarks" "")
 (:dl
  "<"
  "DL"
  "><"
  "p"
  ">"
  (:dt
   "<"
   "DT"
   "><"
   (:tag "H3")
   (:attribute
    (:attributeName "ADD_DATE")
    "="
    (:attributeValue "\"1481473849\""))
   (:attribute
    (:attributeName "LAST_MODIFIED")
    "="
    (:attributeValue "\"1481473992\""))
   (:attribute
    (:attributeName "PERSONAL_TOOLBAR_FOLDER")
    "="
    (:attributeValue "\"true\""))
   ">"
   (:content "Bookmarks" "bar")
   "")
  (:dt
   (:dl
    "<"
    "DL"
    "><"
    "p"
    ">"
    (:dt
     "<"
     "DT"
     "><"
     (:tag "H3")
     (:attribute
      (:attributeName "ADD_DATE")
      "="
      (:attributeValue "\"1481473866\""))
     (:attribute
      (:attributeName "LAST_MODIFIED")
      "="
      (:attributeValue "\"1481473967\""))
     ">"
     (:content "Test" "1")
     "")
    (:dt
     (:dl
      "<"
      "DL"
      "><"
      "p"
      ">"
      (:dt
       "<"
       "DT"
       "><"
       (:tag "A")
       (:attribute
        (:attributeName "HREF")
        "="
        (:attributeValue "\"https://encrypted.google.com/\""))
       (:attribute
        (:attributeName "ADD_DATE")
        "="
        (:attributeValue "\"1481473884\""))
       (:attribute
        (:attributeName "ICON")
        "="
        (:attributeValue "\"\""))
       ">"
       (:content "Google")
       "")
      (:dt
       "<"
       "DT"
       "><"
       (:tag "A")
       (:attribute
        (:attributeName "HREF")
        "="
        (:attributeValue "\"https://yandex.ru/\""))
       (:attribute
        (:attributeName "ADD_DATE")
        "="
        (:attributeValue "\"1481473892\""))
       (:attribute
        (:attributeName "ICON")
        "="
        (:attributeValue "\"\""))
       ">"
       (:content "Яндекс")
       "")
      (:dt
       "<"
       "DT"
       "><"
       (:tag "A")
       (:attribute
        (:attributeName "HREF")
        "="
        (:attributeValue "\"http://example.com/\""))
       (:attribute
        (:attributeName "ADD_DATE")
        "="
        (:attributeValue "\"1481473954\""))
       ">"
       (:content "Example" "Domain")
       "")
      "<"
      "p"
      ">"))
    (:dt
     "<"
     "DT"
     "><"
     (:tag "H3")
     (:attribute
      (:attributeName "ADD_DATE")
      "="
      (:attributeValue "\"1481473872\""))
     (:attribute
      (:attributeName "LAST_MODIFIED")
      "="
      (:attributeValue "\"1481473980\""))
     ">"
     (:content "Test" "2")
     "")
    (:dt
     (:dl
      "<"
      "DL"
      "><"
      "p"
      ">"
      (:dt
       "<"
       "DT"
       "><"
       (:tag "A")
       (:attribute
        (:attributeName "HREF")
        "="
        (:attributeValue "\"https://duckduckgo.com/\""))
       (:attribute
        (:attributeName "ADD_DATE")
        "="
        (:attributeValue "\"1481473902\""))
       (:attribute
        (:attributeName "ICON")
        "="
        (:attributeValue "\"\""))
       ">"
       (:content "DuckDuckGo")
       "")
      (:dt
       "<"
       "DT"
       "><"
       (:tag "A")
       (:attribute
        (:attributeName "HREF")
        "="
        (:attributeValue "\"https://clojure.news/\""))
       (:attribute
        (:attributeName "ADD_DATE")
        "="
        (:attributeValue "\"1481473936\""))
       (:attribute
        (:attributeName "ICON")
        "="
        (:attributeValue "\"\""))
       ">"
       (:content "Clojure" "News")
       "")
      (:dt
       "<"
       "DT"
       "><"
       (:tag "A")
       (:attribute
        (:attributeName "HREF")
        "="
        (:attributeValue "\"http://example.com/\""))
       (:attribute
        (:attributeName "ADD_DATE")
        "="
        (:attributeValue "\"1481473955\""))
       ">"
       (:content "Example" "Domain")
       "")
      "<"
      "p"
      ">"))
    (:dt
     "<"
     "DT"
     "><"
     (:tag "A")
     (:attribute
      (:attributeName "HREF")
      "="
      (:attributeValue "\"https://yandex.ru/\""))
     (:attribute
      (:attributeName "ADD_DATE")
      "="
      (:attributeValue "\"1481473893\""))
     (:attribute (:attributeName "ICON") "=" (:attributeValue "\"\""))
     ">"
     (:content "Яндекс")
     "")
    (:dt
     "<"
     "DT"
     "><"
     (:tag "A")
     (:attribute
      (:attributeName "HREF")
      "="
      (:attributeValue "\"http://www.echojs.com/\""))
     (:attribute
      (:attributeName "ADD_DATE")
      "="
      (:attributeValue "\"1481473986\""))
     (:attribute (:attributeName "ICON") "=" (:attributeValue "\"\""))
     ">")
    (:dt
     "<"
     "DT"
     "><"
     (:tag "A")
     (:attribute
      (:attributeName "HREF")
      "="
      (:attributeValue "\"https://clojure.news/\""))
     (:attribute
      (:attributeName "ADD_DATE")
      "="
      (:attributeValue "\"1481473992\""))
     (:attribute (:attributeName "ICON") "=" (:attributeValue "\"\""))
     ">")
    (:dt
     "<"
     "DT"
     "><"
     (:tag "H3")
     (:attribute
      (:attributeName "ADD_DATE")
      "="
      (:attributeValue "\"1481474004\""))
     (:attribute
      (:attributeName "LAST_MODIFIED")
      "="
      (:attributeValue "\"1481477692\""))
     ">"
     (:content "Test" "3")
     "")
    (:dt
     (:dl
      "<"
      "DL"
      "><"
      "p"
      ">"
      (:dt
       "<"
       "DT"
       "><"
       (:tag "A")
       (:attribute
        (:attributeName "HREF")
        "="
        (:attributeValue "\"https://encrypted.google.com/\""))
       (:attribute
        (:attributeName "ADD_DATE")
        "="
        (:attributeValue "\"1481474004\""))
       (:attribute
        (:attributeName "ICON")
        "="
        (:attributeValue "\"\""))
       ">"
       (:content "Google")
       "")
      (:dt
       "<"
       "DT"
       "><"
       (:tag "A")
       (:attribute
        (:attributeName "HREF")
        "="
        (:attributeValue "\"https://duckduckgo.com/\""))
       (:attribute
        (:attributeName "ADD_DATE")
        "="
        (:attributeValue "\"1481474004\""))
       (:attribute
        (:attributeName "ICON")
        "="
        (:attributeValue "\"\""))
       ">"
       (:content "DuckDuckGo")
       "")
      (:dt
       "<"
       "DT"
       "><"
       (:tag "A")
       (:attribute
        (:attributeName "HREF")
        "="
        (:attributeValue "\"https://clojure.news/\""))
       (:attribute
        (:attributeName "ADD_DATE")
        "="
        (:attributeValue "\"1481474004\""))
       (:attribute
        (:attributeName "ICON")
        "="
        (:attributeValue "\"\""))
       ">"
       (:content "Clojure" "News")
       "")
      (:dt
       "<"
       "DT"
       "><"
       (:tag "H3")
       (:attribute
        (:attributeName "ADD_DATE")
        "="
        (:attributeValue "\"1481477681\""))
       (:attribute
        (:attributeName "LAST_MODIFIED")
        "="
        (:attributeValue "\"1481477681\""))
       ">"
       (:content "Test" "4")
       "")
      (:dt
       (:dl
        "<"
        "DL"
        "><"
        "p"
        ">"
        (:dt
         "<"
         "DT"
         "><"
         (:tag "A")
         (:attribute
          (:attributeName "HREF")
          "="
          (:attributeValue "\"https://clojure.news/\""))
         (:attribute
          (:attributeName "ADD_DATE")
          "="
          (:attributeValue "\"1481477681\""))
         (:attribute
          (:attributeName "ICON")
          "="
          (:attributeValue "\"\""))
         ">"
         (:content "Clojure" "News")
         "")
        (:dt
         "<"
         "DT"
         "><"
         (:tag "A")
         (:attribute
          (:attributeName "HREF")
          "="
          (:attributeValue "\"https://news.ycombinator.com/\""))
         (:attribute
          (:attributeName "ADD_DATE")
          "="
          (:attributeValue "\"1481477681\""))
         (:attribute
          (:attributeName "ICON")
          "="
          (:attributeValue "\"\""))
         ">"
         (:content "Hacker" "News")
         "")
        (:dt
         "<"
         "DT"
         "><"
         (:tag "A")
         (:attribute
          (:attributeName "HREF")
          "="
          (:attributeValue "\"http://example.com/\""))
         (:attribute
          (:attributeName "ADD_DATE")
          "="
          (:attributeValue "\"1481477681\""))
         ">"
         (:content "Example" "Domain")
         "")
        "<"
        "p"
        ">"))
      (:dt
       "<"
       "DT"
       "><"
       (:tag "A")
       (:attribute
        (:attributeName "HREF")
        "="
        (:attributeValue "\"https://news.ycombinator.com/\""))
       (:attribute
        (:attributeName "ADD_DATE")
        "="
        (:attributeValue "\"1481474004\""))
       (:attribute
        (:attributeName "ICON")
        "="
        (:attributeValue "\"\""))
       ">"
       (:content "Hacker" "News")
       "")
      (:dt
       "<"
       "DT"
       "><"
       (:tag "A")
       (:attribute
        (:attributeName "HREF")
        "="
        (:attributeValue "\"http://example.com/\""))
       (:attribute
        (:attributeName "ADD_DATE")
        "="
        (:attributeValue "\"1481474004\""))
       ">"
       (:content "Example" "Domain")
       "")
      "<"
      "p"
      ">"))
    "<"
    "p"
    ">"))
  "<"
  "p"
  ">"))