@@ -30,6 +30,7 @@ defmodule NimbleTOTP do
3030
3131 * Generate secrets composed of random bytes.
3232 * Generate URIs to be encoded in a QR Code.
33+ * Decode otpauth://totp/ URIs and extract the secret, label and issuer.
3334 * Generate Time-Based One-Time Passwords (TOTPs) based on a secret.
3435
3536 ### Generating the secret
@@ -188,6 +189,52 @@ defmodule NimbleTOTP do
188189 otpauth_uri ( issuer , account , secret , [ ] )
189190 end
190191
192+ @ doc """
193+ Extract the secret and label from an otpauth URI, as well as the extra properties.
194+
195+ The issuer will be extracted either from a label prefix or from an extra URI parameter.
196+ If both are present, they have to be equal or an error will be returned.
197+
198+ ## Examples
199+
200+ iex> NimbleTOTP.decompose_otpauth_uri("otpauth://totp/Acme:alice?secret=MFRGGZA&issuer=Acme")
201+ {:ok, "abcd", "alice", %{"issuer" => "Acme"}}
202+
203+ iex> NimbleTOTP.decompose_otpauth_uri("otpauth://totp/Acme:alice?secret=INVALID!&issuer=Acme")
204+ :error
205+
206+ """
207+ @ spec decompose_otpauth_uri ( String . t ( ) ) ::
208+ { :ok , << >> , String . t ( ) , map ( ) } | { :error , :invalid_uri }
209+ def decompose_otpauth_uri ( uri ) when is_binary ( uri ) do
210+ with true <- uri =~ ~r" ^otpauth://totp/" ,
211+ { :ok , uri } <- URI . new ( uri ) ,
212+ [ "" , label ] <- String . split ( uri . path , "/" ) ,
213+ # Reject empty prefix issuer or empty labels (with or without prefix issuer)
214+ false <- label =~ ~r/ ^(:|$|.*:$)/ ,
215+ query = URI . decode_query ( uri . query , % { } , :rfc3986 ) ,
216+ { secret , query } <- Map . pop ( query , "secret" ) ,
217+ { :ok , secret } <- Base . decode32 ( secret , padding: false ) ,
218+ param_issuer <- Map . get ( query , "issuer" ) ,
219+ false <- ( param_issuer || "" ) =~ ":" do
220+ case String . split ( label , ":" ) do
221+ [ label ] when param_issuer != "" ->
222+ { :ok , secret , label , query }
223+
224+ [ ^ param_issuer , label ] ->
225+ { :ok , secret , label , query }
226+
227+ [ issuer , label ] when is_nil ( param_issuer ) ->
228+ { :ok , secret , label , Map . put ( query , "issuer" , issuer ) }
229+
230+ _ ->
231+ :error
232+ end
233+ else
234+ _ -> :error
235+ end
236+ end
237+
191238 @ doc """
192239 Generate a binary composed of random bytes.
193240
@@ -217,7 +264,12 @@ defmodule NimbleTOTP do
217264
218265 ## Examples
219266
220- secret = Base.decode32!("PTEPUGZ7DUWTBGMW4WLKB6U63MGKKMCA")
267+ secret = Base.decode32!("PTEPUGZ7DUWTBGMW4WLKB6U63MGKKMCA", padding: false)
268+ NimbleTOTP.verification_code(secret)
269+ #=> "569777"
270+
271+ uri = "otpauth://Acme:alice?secret=PTEPUGZ7DUWTBGMW4WLKB6U63MGKKMCA"
272+ {:ok, secret, _label, _uri_params} = decompose_otpauth_uri(uri)
221273 NimbleTOTP.verification_code(secret)
222274 #=> "569777"
223275
0 commit comments