profile
viewpoint
If you are wondering where the data of this site comes from, please visit https://api.github.com/users/robbiehanson/events. GitMemory does not store any data, but only uses NGINX to cache data for a period of time. The idea behind GitMemory is simply to give users a better reading experience.

robbiehanson/CocoaAsyncSocket 12043

Asynchronous socket networking library for Mac and iOS

robbiehanson/XMPPFramework 5889

An XMPP Framework in Objective-C for Mac and iOS

robbiehanson/CocoaHTTPServer 5411

A small, lightweight, embeddable HTTP server for Mac OS X or iOS applications

robbiehanson/XcodeColors 2247

XcodeColors allows you to use colors in the Xcode debugging console. It's designed to aid in the debugging process.

orta/cocoapods-keys 1473

A key value store for storing per-developer environment and application keys

robbiehanson/KissXML 851

A replacement for Cocoa's NSXML cluster of classes. Based on libxml. Works on iOS.

codeFi/XcodeLogger 98

Simple, fast, filterable, colorful, flexible and customizable NSLog replacement.

robbiehanson/AlarmClock 50

Alarm Clock app I wrote a decade ago. Releasing source code due to popular demand.

vinthewrench/S4 13

S4Crypto - Security Library 4 used by 4th A Technologies, LLC.

PullRequestReviewEvent

issue commentACINQ/phoenix-kmm

I made a Czech localization

Sorry for the lack of updates. There is currently a TestFlight build available with your localization. (If you don't already have access to our TestFlight beta builds, you can send us an email at phoenix@acinq.co)

The build doesn't have your changes to about.html & advancedSecurity.html. But we are planning on putting out another build very soon (probably this week), which should include those files.

Chuck3CZ

comment created time in 5 days

PR opened ACINQ/phoenix

Adding support for lnurl-pay

Tipping made easy:

Support for comments:

comments

I still need to add support for successAction

+2325 -466

0 comment

14 changed files

pr created time in 6 days

create barnchACINQ/phoenix

branch : lnurl-pay

created branch time in 7 days

create barnchACINQ/phoenix-kmm

branch : lnurl-pay

created branch time in 7 days

issue commentACINQ/phoenix-kmm

I made a Czech localization

Looking good ! We are getting ready to push out a TestFlight build for you.

Screen Shot 2021-09-13 at 08 22 32

There are 2 hidden localized HTML files that are easy to miss here

  • about.html - Displayed via: Settings -> About
  • advancedSecurity.html - Displayed via: Settings -> App Access -> Question Button (?)
Chuck3CZ

comment created time in 14 days

delete branch ACINQ/phoenix-kmm

delete branch : czech-localization

delete time in 14 days

push eventACINQ/phoenix-kmm

Robbie Hanson

commit sha 33a05a3079673539db3e45309eced45a53300b5c

Adding Czech localization

view details

push time in 14 days

create barnchACINQ/phoenix-kmm

branch : czech-localization

created branch time in 14 days

delete branch ACINQ/phoenix-kmm

delete branch : lnurl-auth

delete time in 18 days

delete branch ACINQ/phoenix-kmm

delete branch : lnurl-auth-rebase

delete time in 18 days

push eventACINQ/phoenix-kmm

Robbie Hanson

commit sha 7e1ac0881dad4fc6b54deb52da57d0566a590e69

lnurl-auth support (#202)

view details

push time in 18 days

PR merged ACINQ/phoenix-kmm

lnurl-auth support

This PR builds on previous commits from @dpad85, and adds the UI for lnurl-auth.

lnurl-auth login

+1778 -185

1 comment

21 changed files

robbiehanson

pr closed time in 18 days

push eventACINQ/phoenix-kmm

Robbie Hanson

commit sha cfe7f2a64c07664f1eb5c7f05a7fbeeba7670f7f

Updating url

view details

push time in 18 days

PullRequestReviewEvent

Pull request review commentACINQ/phoenix-kmm

lnurl-auth support

 class AppConfigurationManager(         }     } -    /** The flow containing the configuration to use for Electrum. If null, we do not know what conf to use. */+    private val publicSuffixListKey = "publicSuffixList"++    public suspend fun fetchPublicSuffixList(+        refreshIfOlderThan: Duration = 1.days

Good catch. Done in 83b5d3f

robbiehanson

comment created time in 19 days

PullRequestReviewEvent

Pull request review commentACINQ/phoenix-kmm

lnurl-auth support

+package fr.acinq.phoenix.utils++/* Implements "Public Suffix List" spec:+ * https://publicsuffix.org/list/+ */+class PublicSuffixList(+    list: String+) {+    /**+     * Represents a single "rule".+     * That is, a singe (parsed) line from Public Suffix List.+     */+    class Rule(+        val labels: List<String>,+        val isExceptionRule: Boolean+    ) {+        companion object {+            private val whitespace = "\\s+".toRegex()++            /**+             * Attempts to parse a rule from the given line.+             * If the line is only whitespace or a comment, then returns null.+             */+            fun parse(line: String): Rule? {+                // Definitions:+                // - Each line is only read up to the first whitespace;+                //   entire lines can also be commented using //.+                // - Each line which is not entirely whitespace or begins with a comment contains a rule.+                // - A rule may begin with a "!" (exclamation mark).+                //   If it does, it is labelled as an "exception rule"+                //   and then treated as if the exclamation mark is not present.+                // - A domain or rule can be split into a list of labels using the separator "." (dot).+                //   The separator is not part of any label.+                //   Empty labels are not permitted, meaning that leading and trailing dots are ignored.++                var suffix = line.split(regex = whitespace, limit = 1).firstOrNull() ?: ""+                if (suffix.isEmpty() || suffix.startsWith("//")) {

Done in 83b5d3f

robbiehanson

comment created time in 19 days

Pull request review commentACINQ/phoenix-kmm

lnurl-auth support

+package fr.acinq.phoenix.utils++/* Implements "Public Suffix List" spec:+ * https://publicsuffix.org/list/+ */+class PublicSuffixList(+    list: String+) {+    /**+     * Represents a single "rule".+     * That is, a singe (parsed) line from Public Suffix List.+     */+    class Rule(+        val labels: List<String>,+        val isExceptionRule: Boolean+    ) {+        companion object {+            private val whitespace = "\\s+".toRegex()++            /**+             * Attempts to parse a rule from the given line.+             * If the line is only whitespace or a comment, then returns null.+             */+            fun parse(line: String): Rule? {+                // Definitions:+                // - Each line is only read up to the first whitespace;+                //   entire lines can also be commented using //.+                // - Each line which is not entirely whitespace or begins with a comment contains a rule.+                // - A rule may begin with a "!" (exclamation mark).+                //   If it does, it is labelled as an "exception rule"+                //   and then treated as if the exclamation mark is not present.+                // - A domain or rule can be split into a list of labels using the separator "." (dot).+                //   The separator is not part of any label.+                //   Empty labels are not permitted, meaning that leading and trailing dots are ignored.++                var suffix = line.split(regex = whitespace, limit = 1).firstOrNull() ?: ""+                if (suffix.isEmpty() || suffix.startsWith("//")) {+                    return null+                }++                var isExceptionRule = false+                if (suffix.startsWith('!')) {+                    isExceptionRule = true+                    suffix = suffix.substring(1)+                }++                val labels = suffix.split('.').filter {+                    it.isNotEmpty()

Done in 83b5d3f

robbiehanson

comment created time in 19 days

PullRequestReviewEvent

push eventACINQ/phoenix-kmm

Robbie Hanson

commit sha 83b5d3f04fd66250e6e475d03a0b2d462a89ab7c

Minor changes based on feedback

view details

push time in 19 days

Pull request review commentACINQ/phoenix-kmm

lnurl-auth support

+/*+ * Copyright 2021 ACINQ SAS+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ *     http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package fr.acinq.phoenix.managers++import fr.acinq.bitcoin.ByteVector32+import fr.acinq.bitcoin.Crypto+import fr.acinq.lightning.utils.Either+import fr.acinq.phoenix.PhoenixBusiness+import fr.acinq.phoenix.data.LNUrl+import io.ktor.client.*+import io.ktor.client.request.*+import io.ktor.client.statement.*+import io.ktor.http.*+import kotlinx.coroutines.CoroutineScope+import kotlinx.coroutines.MainScope+import kotlinx.coroutines.flow.filterNotNull+import kotlinx.coroutines.flow.first+import org.kodein.log.LoggerFactory+import org.kodein.log.newLogger++class LNUrlManager(+    loggerFactory: LoggerFactory,+    private val httpClient: HttpClient,+    private val walletManager: WalletManager+) : CoroutineScope by MainScope() {++    constructor(business: PhoenixBusiness) : this(+        loggerFactory = business.loggerFactory,+        httpClient = business.httpClient,+        walletManager = business.walletManager+    )++    private val log = newLogger(loggerFactory)++    /**+     * Get the LNUrl for this source. Throw exception if source is malformed, or invalid.+     * Will execute an HTTP request for some urls and parse the response into an actionable LNUrl object.+     */+    suspend fun extractLNUrl(source: String): LNUrl {+        return when (val result = interactiveExtractLNUrl(source)) {+            is Either.Left -> result.value+            is Either.Right -> continueLnUrl(result.value)+        }+    }++    /**+     * Attempts to extract a Bech32 URL from the string.+     * On success:+     * - if the LnUrl is a simple Auth, it's returned immediately+     * - otherwise the Url is returned (call `continueLnUrl` to fetch & parse the Url)+     *+     * Throws an exception if source is malformed, or invalid.+     */+    fun interactiveExtractLNUrl(source: String): Either<LNUrl.Auth, Url> {+        val url = try {+            LNUrl.parseBech32Url(source)+        } catch (e1: Exception) {+            log.debug { "cannot parse source=$source as a bech32 lnurl" }+            try {+                LNUrl.parseNonBech32Url(source)+            } catch (e2: Exception) {+                log.error { "cannot extract lnurl from source=$source: ${e1.message ?: e1::class} / ${e2.message ?: e2::class}"}+                throw LNUrl.Error.Invalid+            }+        }+        return when (url.parameters["tag"]) {+            // auth urls must not be called just yet+            LNUrl.Tag.Auth.label -> {+                val k1 = url.parameters["k1"]+                if (k1.isNullOrBlank()) {+                    throw LNUrl.Error.Auth.MissingK1+                } else {+                    Either.Left(LNUrl.Auth(url, k1))+                }+            }+            else -> Either.Right(url)+        }+    }++    /**+     * Executes the HTTP request for the LnUrl and parses the response+     * into an actionable LNUrl object.+     */+    suspend fun continueLnUrl(url: Url): LNUrl {+        val json = LNUrl.handleLNUrlResponse(httpClient.get(url))+        return LNUrl.parseLNUrlMetadata(json)+    }++    suspend fun requestAuth(auth: LNUrl.Auth) {+        val wallet = walletManager.wallet.filterNotNull().first()++        // According to the spec, the "full domain name" is to be used for key derivation:+        // > LN SERVICE should carefully choose which subdomain (if any) will be used as+        // > LNURL-auth endpoint and stick to chosen subdomain in future. For example,+        // > if `auth.site.com` was initially chosen then changing it to, say,+        // > `login.site.com` will result in different account for each user because+        // > full domain name is used by wallets as material for key derivation.+        // >+        // > LN SERVICE should consider giving meaningful names to chosen subdomains+        // > since LN WALLET may show a full domain name to users on login attempt.+        // > For example, `auth.site.com` is less confusing than `ksf03.site.com`.+        //+        // Spec: https://github.com/fiatjaf/lnurl-rfc/blob/luds/04.md+        //+        val domain = auth.url.host

Done in c035bf0

robbiehanson

comment created time in 20 days

PullRequestReviewEvent

pull request commentACINQ/phoenix-kmm

lnurl-auth support

Support for Public Suffix List has been added to shared module. Here's how it works:

  • When ScanController detects a lnurl-auth, it starts a pre-fetch of the Public Suffix List (PSL)
  • When asked to authenticate, the PSL is consulted in order to extract the eTLD+1
  • The fetched PSL is cached in the database (and only re-fetched if older than 24 hours)

Unit tests have been added for the PSL logic. Some notes:

The PSL list is really big. It would probably be more performant if we cached this as a file. However, I think we need a 3rd party library to do basic filesystem IO in Kotlin Native. (Maybe Okio) So the simpler solution was to store it in the database. It would be nice to compress the data we store in the database. But I think we need a 3rd party library to do that too...

Since we're adding another table to the database, I took this opportunity to add a simple key/value table that can be re-used for other things in the future.

I had to disable some unit tests because we aren't performing proper IDN on the domain name. In particular, there is a set of unit tests that require punycode support. It looks like this exists in java.net.IDN, but not in Kotlin native.

robbiehanson

comment created time in 20 days

Pull request review commentACINQ/phoenix-kmm

lnurl-auth support

 struct SendingView: View { 			NSLocalizedString("Sending payment", comment: "Navigation bar title"), 			displayMode: .inline 		)-		.zIndex(0) // [SendingView, ValidateView, ScanView]+		.zIndex(0) // [SendingView, ValidateView, LoginView, ScanView]+	}+}++struct LoginView: View, ViewName {+	+	@ObservedObject var mvi: MVIState<Scan.Model, Scan.Intent>+	+	enum MaxImageHeight: Preference {}+	let maxImageHeightReader = GeometryPreferenceReader(+		key: AppendValue<MaxImageHeight>.self,+		value: { [$0.size.height] }+	)+	@State var maxImageHeight: CGFloat? = nil+	+	@ViewBuilder+	var body: some View {+		+		ZStack {+		+			if AppDelegate.showTestnetBackground {+				Image("testnet_bg")+					.resizable(resizingMode: .tile)+			}+			+			// I want the height of these 2 components to match exactly:+			// Button("<img> Login")+			// HStack("<img> Logged In")+			//+			// To accomplish this, I need the images to be same height.+			// But they're not - unless we measure them, and enforce matching heights.+			+			Image(systemName: "bolt")+				.imageScale(.large)+				.font(.title2)+				.foregroundColor(.clear)+				.read(maxImageHeightReader)+			+			Image(systemName: "hand.thumbsup.fill")+				.imageScale(.large)+				.font(.title2)+				.foregroundColor(.clear)+				.read(maxImageHeightReader)+			+			main+		}+		.assignMaxPreference(for: maxImageHeightReader.key, to: $maxImageHeight)+		.frame(maxHeight: .infinity)+		.background(Color.primaryBackground)+		.edgesIgnoringSafeArea([.bottom, .leading, .trailing]) // top is nav bar+		.navigationBarTitle(+			NSLocalizedString("lnurl-auth", comment: "Navigation bar title"),+			displayMode: .inline+		)+		.zIndex(2) // [SendingView, ValidateView, LoginView, ScanView]+	}+	+	@ViewBuilder+	var main: some View {+		+		VStack(alignment: HorizontalAlignment.center, spacing: 30) {+			+			Spacer()+			+			Text("You can use your wallet to anonymously sign and authorize an action on:")+				.multilineTextAlignment(.center)+			+			Text(domain())+				.font(.headline)+				.multilineTextAlignment(.center)+			+			if let model = mvi.model as? Scan.ModelLoginResult, model.error == nil {+				+				HStack(alignment: VerticalAlignment.firstTextBaseline) {+					Image(systemName: "hand.thumbsup.fill")+						.renderingMode(.template)+						.imageScale(.large)+						.frame(minHeight: maxImageHeight)+					Text(successTitle())+				}+				.font(.title2)+				.foregroundColor(Color.appPositive)+				.padding(.top, 4)+				.padding(.bottom, 5)+				.padding([.leading, .trailing], 24)+				+			} else {+				+				Button {

Done in 163dc42

robbiehanson

comment created time in 20 days

PullRequestReviewEvent

push eventACINQ/phoenix-kmm

Robbie Hanson

commit sha 163dc424ef5d151853abccf6eb7b3f7bd0189779

Reducing size of button

view details

push time in 20 days

push eventACINQ/phoenix-kmm

Robbie Hanson

commit sha a11604c0a2ee6210ef2dc13ce9babe605ad66659

Adding unit tests for Public Suffix List

view details

Robbie Hanson

commit sha d2212686fb03f38087982a613ee2f2cafed1bd63

Adding migration script for SQLDelight

view details

push time in 20 days

push eventACINQ/phoenix-kmm

Robbie Hanson

commit sha c035bf0a88134438a10dcd8d56a193432ff0970b

Adding "public suffix list" proof-of-concept. Still needs unit tests. And a migration script for SQLDelight.

view details

push time in 25 days

Pull request review commentACINQ/phoenix-kmm

lnurl-auth support

+/*+ * Copyright 2021 ACINQ SAS+ *+ * Licensed under the Apache License, Version 2.0 (the "License");+ * you may not use this file except in compliance with the License.+ * You may obtain a copy of the License at+ *+ *     http://www.apache.org/licenses/LICENSE-2.0+ *+ * Unless required by applicable law or agreed to in writing, software+ * distributed under the License is distributed on an "AS IS" BASIS,+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ * See the License for the specific language governing permissions and+ * limitations under the License.+ */++package fr.acinq.phoenix.managers++import fr.acinq.bitcoin.ByteVector32+import fr.acinq.bitcoin.Crypto+import fr.acinq.lightning.utils.Either+import fr.acinq.phoenix.PhoenixBusiness+import fr.acinq.phoenix.data.LNUrl+import io.ktor.client.*+import io.ktor.client.request.*+import io.ktor.client.statement.*+import io.ktor.http.*+import kotlinx.coroutines.CoroutineScope+import kotlinx.coroutines.MainScope+import kotlinx.coroutines.flow.filterNotNull+import kotlinx.coroutines.flow.first+import org.kodein.log.LoggerFactory+import org.kodein.log.newLogger++class LNUrlManager(+    loggerFactory: LoggerFactory,+    private val httpClient: HttpClient,+    private val walletManager: WalletManager+) : CoroutineScope by MainScope() {++    constructor(business: PhoenixBusiness) : this(+        loggerFactory = business.loggerFactory,+        httpClient = business.httpClient,+        walletManager = business.walletManager+    )++    private val log = newLogger(loggerFactory)++    /**+     * Get the LNUrl for this source. Throw exception if source is malformed, or invalid.+     * Will execute an HTTP request for some urls and parse the response into an actionable LNUrl object.+     */+    suspend fun extractLNUrl(source: String): LNUrl {+        return when (val result = interactiveExtractLNUrl(source)) {+            is Either.Left -> result.value+            is Either.Right -> continueLnUrl(result.value)+        }+    }++    /**+     * Attempts to extract a Bech32 URL from the string.+     * On success:+     * - if the LnUrl is a simple Auth, it's returned immediately+     * - otherwise the Url is returned (call `continueLnUrl` to fetch & parse the Url)+     *+     * Throws an exception if source is malformed, or invalid.+     */+    fun interactiveExtractLNUrl(source: String): Either<LNUrl.Auth, Url> {+        val url = try {+            LNUrl.parseBech32Url(source)+        } catch (e1: Exception) {+            log.debug { "cannot parse source=$source as a bech32 lnurl" }+            try {+                LNUrl.parseNonBech32Url(source)+            } catch (e2: Exception) {+                log.error { "cannot extract lnurl from source=$source: ${e1.message ?: e1::class} / ${e2.message ?: e2::class}"}+                throw LNUrl.Error.Invalid+            }+        }+        return when (url.parameters["tag"]) {+            // auth urls must not be called just yet+            LNUrl.Tag.Auth.label -> {+                val k1 = url.parameters["k1"]+                if (k1.isNullOrBlank()) {+                    throw LNUrl.Error.Auth.MissingK1+                } else {+                    Either.Left(LNUrl.Auth(url, k1))+                }+            }+            else -> Either.Right(url)+        }+    }++    /**+     * Executes the HTTP request for the LnUrl and parses the response+     * into an actionable LNUrl object.+     */+    suspend fun continueLnUrl(url: Url): LNUrl {+        val json = LNUrl.handleLNUrlResponse(httpClient.get(url))+        return LNUrl.parseLNUrlMetadata(json)+    }++    suspend fun requestAuth(auth: LNUrl.Auth) {+        val wallet = walletManager.wallet.filterNotNull().first()++        // According to the spec, the "full domain name" is to be used for key derivation:+        // > LN SERVICE should carefully choose which subdomain (if any) will be used as+        // > LNURL-auth endpoint and stick to chosen subdomain in future. For example,+        // > if `auth.site.com` was initially chosen then changing it to, say,+        // > `login.site.com` will result in different account for each user because+        // > full domain name is used by wallets as material for key derivation.+        // >+        // > LN SERVICE should consider giving meaningful names to chosen subdomains+        // > since LN WALLET may show a full domain name to users on login attempt.+        // > For example, `auth.site.com` is less confusing than `ksf03.site.com`.+        //+        // Spec: https://github.com/fiatjaf/lnurl-rfc/blob/luds/04.md+        //+        val domain = auth.url.host

That's a good point. Web services will undoubtably change their sub-domain. And if they do so while lnurl-auth is young, and has relatively few users on their site (compared to other offered login options, such as Facebook), then the website is unlikely to fix their mistake. The wallet will surely receive the blame, both by the web service and the user. And we will gain little ground in the argument by deferring to the spec.

eTLD+1

This sounds like a logical solution. And, from a user's perspective, is probably what I would expect.

I will work on adding code to fetch & parse the public list.

robbiehanson

comment created time in a month