1defmodule Comeonin.Bcrypt.Base64 do
2  @moduledoc """
3  Module that provides base64 encoding for bcrypt.
4
5  Bcrypt uses an adapted base64 alphabet (using `.` instead of `+`,
6  starting with `./` and with no padding).
7  """
8
9  use Bitwise
10
11  @decode_map {:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:ws,:ws,:bad,:bad,:ws,:bad,:bad,
12    :bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,
13    :ws,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,0,1,
14    54,55,56,57,58,59,60,61,62,63,:bad,:bad,:bad,:eq,:bad,:bad,
15    :bad,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,
16    17,18,19,20,21,22,23,24,25,26,27,:bad,:bad,:bad,:bad,:bad,
17    :bad,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,
18    43,44,45,46,47,48,49,50,51,52,53,:bad,:bad,:bad,:bad,:bad,
19    :bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,
20    :bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,
21    :bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,
22    :bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,
23    :bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,
24    :bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,
25    :bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,
26    :bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad,:bad}
27
28  @doc """
29  Encode using the adapted Bcrypt alphabet.
30
31  ## Examples
32
33      iex> Comeonin.Bcrypt.Base64.encode 'spamandeggs'
34      'a1/fZUDsXETlX1K'
35  """
36  def encode(words), do: encode_l(words)
37
38  @doc """
39  Decode using the adapted Bcrypt alphabet.
40
41  ## Examples
42
43      iex> Comeonin.Bcrypt.Base64.decode 'a1/fZUDsXETlX1K'
44      'spamandeggs'
45  """
46  def decode(words), do: decode_l(words, [])
47
48  @doc """
49  Shorten the salt to 128 bits + 4 zeros.
50
51  ## Examples
52
53      iex> Comeonin.Bcrypt.Base64.normalize '0CaBPBIAm2pgeQiCrTSa5O'
54      '0CaBPBIAm2pgeQiCrTSa5O'
55      iex> Comeonin.Bcrypt.Base64.normalize '0CaBPBIAm2pgeQiCrTSa5S'
56      '0CaBPBIAm2pgeQiCrTSa5O'
57      iex> Comeonin.Bcrypt.Base64.normalize '0CaBPBIAm2pgeQiCrTSa5B'
58      '0CaBPBIAm2pgeQiCrTSa5.'
59  """
60  def normalize(salt) do
61    decode(salt) |> encode
62  end
63
64  defp b64e(val) do
65    elem({?., ?/, ?A, ?B, ?C, ?D, ?E, ?F, ?G, ?H, ?I, ?J, ?K, ?L,
66      ?M, ?N, ?O, ?P, ?Q, ?R, ?S, ?T, ?U, ?V, ?W, ?X,
67      ?Y, ?Z, ?a, ?b, ?c, ?d, ?e, ?f, ?g, ?h, ?i, ?j, ?k, ?l,
68      ?m, ?n, ?o, ?p, ?q, ?r, ?s, ?t, ?u, ?v, ?w, ?x,
69      ?y, ?z, ?0, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9}, val)
70  end
71
72  defp encode_l([]), do: []
73  defp encode_l([a]) do
74    [b64e(a >>> 2),
75      b64e((a &&& 3) <<< 4)]
76  end
77  defp encode_l([a,b]) do
78    [b64e(a >>> 2),
79      b64e(((a &&& 3) <<< 4) ||| (b >>> 4)),
80      b64e((b &&& 15) <<< 2)]
81  end
82  defp encode_l([a,b,c|ls]) do
83    bb = (a <<< 16) ||| (b <<< 8) ||| c
84    [b64e(bb >>> 18),
85      b64e((bb >>> 12) &&& 63),
86      b64e((bb >>> 6) &&& 63),
87      b64e(bb &&& 63) | encode_l(ls)]
88  end
89
90  defp decode_l([], a), do: a
91  defp decode_l([c1,c2], a) do
92    bits2x6 = (b64d(c1) <<< 18) ||| (b64d(c2) <<< 12)
93    octet1 = bits2x6 >>> 16
94    a ++ [octet1]
95  end
96  defp decode_l([c1,c2,c3], a) do
97    bits3x6 = (b64d(c1) <<< 18) ||| (b64d(c2) <<< 12) ||| (b64d(c3) <<< 6)
98    octet1 = bits3x6 >>> 16
99    octet2 = (bits3x6 >>> 8) &&& 0xff
100    a ++ [octet1,octet2]
101  end
102  defp decode_l([c1,c2,c3,c4| cs], a) do
103    bits4x6 = (b64d(c1) <<< 18) ||| (b64d(c2) <<< 12) ||| (b64d(c3) <<< 6) ||| b64d(c4)
104    octet1 = bits4x6 >>> 16
105    octet2 = (bits4x6 >>> 8) &&& 0xff
106    octet3 = bits4x6 &&& 0xff
107    decode_l(cs, a ++ [octet1,octet2,octet3])
108  end
109
110  defp b64d(val) do
111    b64d_ok(elem(@decode_map, val))
112  end
113
114  defp b64d_ok(val) when is_integer(val), do: val
115  defp b64d_ok(val) do
116    raise ArgumentError, "Invalid character: #{val}"
117  end
118end
119