Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Client-side validation of personal numbers #10

Open
anfly0 opened this issue Aug 16, 2020 · 4 comments
Open

Client-side validation of personal numbers #10

anfly0 opened this issue Aug 16, 2020 · 4 comments
Labels
enhancement New feature or request help wanted Extra attention is needed

Comments

@anfly0
Copy link
Owner

anfly0 commented Aug 16, 2020

Add personal number validation to the new methods in ExBankID.Auth.Payload and ExBankID.Sign.Payload.

@anfly0 anfly0 added enhancement New feature or request help wanted Extra attention is needed labels Aug 16, 2020
@anfly0 anfly0 modified the milestone: v0.2.0 Aug 16, 2020
@kwando
Copy link

kwando commented Aug 18, 2020

Here is some code for validating and calculating the checksum of Swedish personal numbers using Luhns algorithm.

There are 2 versions available, one recursive and one based on Enum.

defmodule LuhnChecker do
  def valid?(number) when is_integer(number) and number >= 0 do
    digits = Enum.reverse(Integer.digits(number))
    rem(sum_digits(digits, 1), 10) === 0
  end

  def checksum(number) when is_integer(number) and number >= 0 do
    calculate_checksum(Integer.digits(number))
  end

  # alternate solution to calculating the checksum
  defp checksum1(digits) do
    sum =
      digits
      |> Enum.zip(Stream.cycle([2, 1]))
      |> Enum.flat_map(fn {digit, factor} -> (digit * factor) |> Integer.digits() end)
      |> Enum.sum()

    ceil(sum / 10) * 10 - sum
  end

  defp calculate_checksum(digits) do
    sum = sum_digits(digits, 2)
    ceil(sum / 10) * 10 - sum
  end

  defp sum_digits([], _), do: 0

  defp sum_digits([digit | digits], factor) do
    sum_digits(
      digits,
      next_factor(factor)
    ) + checksum_digit(digit * factor)
  end

  defp checksum_digit(digit) when digit <= 9, do: digit
  defp checksum_digit(digit), do: digit - 9

  defp next_factor(1), do: 2
  defp next_factor(2), do: 1
end

@anfly0
Copy link
Owner Author

anfly0 commented Aug 18, 2020

First of all, thank you, @kwando, for taking the time.
This looks very promising, and I would be happy to merge some version of this.
If you want and have the time, could you wrap this up in a PR?
Just decide on what version you think is the cleanest and add some basic tests.

If you would like to take a crack at the rest of this issue, you're of course more than welcome to do so.

@carlgleisner
Copy link

There is actually already a HEX package for validating strings of numbers based on Luhn's algorithm: https://hex.pm/packages/luhn.

I threw together the following example that validates:

  1. The century and
  2. The Luhn checksum
def check_personal_number(personal_number)
    when is_binary(personal_number) do
  with true <- String.length(personal_number) == 12,
       true <- check_personal_number_century(personal_number),
       true <- personal_number |> String.slice(2, 10) |> Luhn.valid?() do
    {:ok, personal_number}
  else
    false -> {:error, "Invalid personal number: #{personal_number}"}
  end
end

def check_personal_number(nil) do
  {:ok, nil}
end

defp check_personal_number_century("19" <> _), do: true
defp check_personal_number_century("20" <> _), do: true
defp check_personal_number_century(_), do: false

Then of course, one could validate that the month and day are reasonable numbers. My mind goes to NimbleParsec and this Elixir Forum topic. Now, needless to say, there is the Gregorian calendar to observe in this regard. But, I guess that a rudimentary check is better than nothing to begin with?

For testing there's the Swedish Tax Authority's published personal numbers reserved for testing. I guess one would want tests that cover the cases. With the current ones all Luhn validations will be performed on mere zeros.

@carlgleisner
Copy link

First stab. No previous experience in writing parsers so take it for what it is.

defmodule MyParser do
  import NimbleParsec

  century =
    choice([string("19"), string("20")])
  
  year =
    integer(2)
  
  month =
    ~w(01 02 03 04 05 06 07 08 09 10 11 12)
    |> Enum.map(&string/1)
    |> choice()
  
  day =
    (~w(01 02 03 04 05 06 07 08 09) ++ Enum.map(10..31, &to_string/1))
    |> Enum.map(&string/1)
    |> choice()
  
  defparsec :personal_number, century |> concat(year) |> concat(month) |> concat(day)
end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

3 participants