package sihl

  1. Overview
  2. Docs
Legend:
Page
Library
Module
Module type
Parameter
Class
Class type
Source

Source file sihl_utils.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
module Core = Sihl_core

module Time = struct
  type duration =
    | OneSecond
    | OneMinute
    | TenMinutes
    | OneHour
    | OneDay
    | OneWeek
    | OneMonth
    | OneYear
  [@@deriving yojson, show, eq]

  let duration_to_span duration =
    let duration_s =
      match duration with
      | OneSecond -> 1.
      | OneMinute -> 60.
      | TenMinutes -> 60. *. 10.
      | OneHour -> 60. *. 60.
      | OneDay -> 60. *. 60. *. 24.
      | OneWeek -> 60. *. 60. *. 24. *. 7.
      | OneMonth -> 60. *. 60. *. 24. *. 30.
      | OneYear -> 60. *. 60. *. 24. *. 365.
    in
    Option.get (Ptime.of_float_s duration_s) |> Ptime.to_span
  ;;

  let ptime_to_yojson ptime = `String (Ptime.to_rfc3339 ptime)

  let ptime_of_yojson yojson =
    match
      yojson |> Yojson.Safe.to_string |> Ptime.of_rfc3339 |> Ptime.rfc3339_error_to_msg
    with
    | Ok (ptime, _, _) -> Ok ptime
    | Error (`Msg msg) -> Error msg
  ;;

  let ptime_of_date_string date =
    let date =
      date
      |> String.split_on_char '-'
      |> List.map int_of_string_opt
      |> List.map
           (Option.to_result
              ~none:
                "Invalid date string provided, make sure that year, month and date are \
                 ints")
      |> List.fold_left
           (fun result item ->
             match item with
             | Ok item -> Result.map (List.cons item) result
             | Error msg -> Error msg)
           (Ok [])
      |> Result.map List.rev
    in
    match date with
    | Ok [ year; month; day ] ->
      Ptime.of_date (year, month, day)
      |> Option.to_result
           ~none:"Invalid date provided, only format 1990-12-01 is accepted"
    | Ok _ -> Error "Invalid date provided, only format 1990-12-01 is accepted"
    | Error msg -> Error msg
  ;;

  let ptime_to_date_string ptime =
    let year, month, day = Ptime.to_date ptime in
    let month =
      if month < 10 then Printf.sprintf "0%d" month else Printf.sprintf "%d" month
    in
    let day = if day < 10 then Printf.sprintf "0%d" day else Printf.sprintf "%d" day in
    Printf.sprintf "%d-%s-%s" year month day
  ;;
end

module Jwt = struct
  type algorithm = Jwto.algorithm =
    | HS256
    | HS512
    | Unknown

  type t = Jwto.t
  type payload = (string * string) list

  let empty = []
  let add_claim ~key ~value payload = Base.List.cons (key, value) payload

  let set_expires_in ~now duration payload =
    let span = Time.duration_to_span duration in
    let epoch_s =
      Ptime.add_span now span |> Option.map Ptime.to_float_s |> Option.map Float.to_string
    in
    match epoch_s with
    | Some epoch_s -> add_claim ~key:"exp" ~value:epoch_s payload
    | None -> payload
  ;;

  let encode algorithm ~secret payload = Jwto.encode algorithm secret payload
  let decode ~secret token = Jwto.decode_and_verify secret token
  let get_claim ~key token = token |> Jwto.get_payload |> Jwto.get_claim key

  let is_expired ~now ?(claim = "exp") token =
    let is_ealier =
      let ( let* ) = Option.bind in
      let* claim = get_claim ~key:claim token in
      let* exp = Float.of_string_opt claim in
      let* exp = Ptime.of_float_s exp in
      Option.some (Ptime.is_earlier exp ~than:now)
    in
    Option.value is_ealier ~default:false
  ;;

  let pp = Jwto.pp
  let eq = Jwto.eq

  module Jwto = Jwto
end

module Json = struct
  type t = Yojson.Safe.t

  let parse str =
    try Ok (str |> Yojson.Safe.from_string) with
    | _ -> Error "failed to parse json"
  ;;

  let parse_opt str =
    try Some (str |> Yojson.Safe.from_string) with
    | _ -> None
  ;;

  let parse_exn str = str |> Yojson.Safe.from_string
  let to_string = Yojson.Safe.to_string

  module Yojson = Yojson.Safe
end

module Regex = struct
  type t = Re.Pcre.regexp

  let of_string string = Re.Pcre.regexp string
  let test regexp string = Re.Pcre.pmatch ~rex:regexp string

  let extract_last regexp text =
    let ( let* ) = Option.bind in
    let extracts = Array.to_list (Re.Pcre.extract ~rex:regexp text) in
    let* extracts =
      try Some (List.tl extracts) with
      | _ -> None
    in
    try Some (List.hd extracts) with
    | _ -> None
  ;;

  module Re = Re
end

module Hashing = struct
  let hash ?count plain =
    match count, Core.Configuration.is_testing () with
    | _, true -> Ok (Bcrypt.hash ~count:4 plain |> Bcrypt.string_of_hash)
    | Some count, false ->
      if count < 4 || count > 31
      then Error "Password hashing count has to be between 4 and 31"
      else Ok (Bcrypt.hash ~count plain |> Bcrypt.string_of_hash)
    | None, false -> Ok (Bcrypt.hash ~count:10 plain |> Bcrypt.string_of_hash)
  ;;

  let matches ~hash ~plain = Bcrypt.verify plain (Bcrypt.hash_of_string hash)

  module Bcrypt = Bcrypt
end

module String = struct
  let strip_chars s cs =
    let len = Caml.String.length s in
    let res = Bytes.create len in
    let rec aux i j =
      if i >= len
      then Bytes.to_string (Bytes.sub res 0 j)
      else if Caml.String.contains cs s.[i]
      then aux (succ i) j
      else (
        Bytes.set res j s.[i];
        aux (succ i) (succ j))
    in
    aux 0 0
  ;;
end

module Encryption = struct
  let xor c1 c2 =
    try
      Some
        (List.map2 (fun chr1 chr2 -> Char.chr (Char.code chr1 lxor Char.code chr2)) c1 c2)
    with
    | exn ->
      Logs.err (fun m ->
          m
            "XOR: Failed to XOR %s and %s. %s"
            (c1 |> List.to_seq |> Caml.String.of_seq)
            (c2 |> List.to_seq |> Caml.String.of_seq)
            (Printexc.to_string exn));
      None
  ;;

  let decrypt_with_salt ~salted_cipher ~salt_length =
    if List.length salted_cipher - salt_length != salt_length
    then (
      Logs.err (fun m ->
          m
            "ENCRYPT: Failed to decrypt cipher %s. Salt length does not match cipher \
             length."
            (salted_cipher |> List.to_seq |> Caml.String.of_seq));
      None)
    else (
      try
        let salt = CCList.take salt_length salted_cipher in
        let encrypted_value = CCList.drop salt_length salted_cipher in
        xor salt encrypted_value
      with
      | exn ->
        Logs.err (fun m ->
            m
              "ENCRYPT: Failed to decrypt cipher %s. %s"
              (salted_cipher |> List.to_seq |> Caml.String.of_seq)
              (Printexc.to_string exn));
        None)
  ;;
end
OCaml

Innovation. Community. Security.