CWIS Developer Documentation
RestAPIHelper.php
Go to the documentation of this file.
1 <?PHP
2 #
3 # FILE: RestAPIHelper
4 #
5 # Part of the Collection Workflow Integration System (CWIS)
6 # Copyright 2017 Edward Almasy and Internet Scout Research Group
7 # http://scout.wisc.edu/cwis/
8 #
9 
20 {
31  public function __construct($APIUrl, $APIPassword,
32  $CheckForDuplicateFn, $RegisterMessageFn)
33  {
34  $this->APIUrl = $APIUrl;
35  $this->APIPassword = $APIPassword;
36  $this->CheckForDuplicateFn = $CheckForDuplicateFn;
37  $this->RegisterMessageFn = $RegisterMessageFn;
38  }
39 
46  public function DoRestCommand($Params)
47  {
48  # build an encrypted message
49  $PostData = $this->EncodeEncryptedMessage($Params);
50 
51  # set up curl to do the post
52  $Context = curl_init();
53 
54  # enable cookie handling
55  curl_setopt($Context, CURLOPT_COOKIEFILE, '');
56 
57  # use our configured endpoint
58  curl_setopt($Context, CURLOPT_URL, $this->APIUrl);
59 
60  # get results back as a string
61  curl_setopt($Context, CURLOPT_RETURNTRANSFER, TRUE);
62 
63  # send data in a POST
64  curl_setopt($Context, CURLOPT_POST, TRUE);
65 
66  # load the POST data
67  curl_setopt($Context, CURLOPT_POSTFIELDS, http_build_query($PostData));
68 
69  # fetch the data
70  $Data = curl_exec($Context);
71 
72  # attempt to parse the reply into an encrypted envelope
73  $Result = json_decode($Data, TRUE);
74  if ($Result === NULL)
75  {
76  return array(
77  "Status" => "Error",
78  "Message" => "Could not parse PostData.",
79  "Data" => $Data,
80  );
81  }
82 
83  # attempt to decode the encrypted envelope
84  $Result = $this->DecodeEncryptedMessage($Result);
85 
86  # if we decoded the envelope, return the message contents
87  if ($Result["Status"] == "OK")
88  {
89  $Result = $Result["Data"];
90  }
91 
92  return $Result;
93  }
94 
100  public function EncodeEncryptedMessage($Data)
101  {
102  # create an envelope for our message, put the provided data inside
103  $Env = array();
104  $Env["Version"] = "3";
105  $Env["Timestamp"] = time();
106  $Env["Cookie"] = base64_encode(random_bytes(16));
107  $Env["Data"] = $Data;
108 
109  # generate full key from provided password
110  $FullKey = hash("sha512", $this->APIPassword, TRUE);
111 
112  # split into encryption and MAC keys
113  $EncKey = mb_substr($FullKey, 0, 32, '8bit');
114  $MacKey = mb_substr($FullKey, 32, 32, '8bit');
115 
116  # generate a random IV (initialization vector), required by
117  # AES (and for most ciphers) to provide some randomness in the
118  # data and prevent identical messages from having identical
119  # encrypted content
120 
121  $IV = random_bytes(16);
122 
123  # encrypt and base64 our payload
124  $Payload = base64_encode(openssl_encrypt(
125  json_encode($Env), "aes-256-cbc", $EncKey, OPENSSL_RAW_DATA, $IV));
126 
127  # base64 encode our IV
128  $IV = base64_encode($IV);
129 
130  # construct data we will POST
131  $PostData = array(
132  "IV" => $IV,
133  "Payload" => $Payload,
134  "MAC" => base64_encode(hash_hmac("sha256", $IV.":".$Payload, $MacKey, TRUE)),
135  );
136 
137  return $PostData;
138  }
139 
149  public function DecodeEncryptedMessage($PostData)
150  {
151  # verify that the provided POST data has the correct elements
152  if (!isset($PostData["MAC"]) || !isset($PostData["Payload"]) ||
153  !isset($PostData["IV"]))
154  {
155  return array(
156  "Status" => "Error",
157  "Message" => "PostData lacks required elements.");
158  }
159 
160  # generate full key from provided password
161  $FullKey = hash("sha512", $this->APIPassword, TRUE);
162 
163  # split into encryption and MAC keys
164  $EncKey = mb_substr($FullKey, 0, 32, '8bit');
165  $MacKey = mb_substr($FullKey, 32, 32, '8bit');
166 
167  # compute MAC
168  $MAC = hash_hmac(
169  "sha256", $PostData["IV"].":".$PostData["Payload"], $MacKey, TRUE);
170 
171  # check MAC, bail if it was not valid
172  if (!hash_equals($MAC, base64_decode($PostData["MAC"])))
173  {
174  return array(
175  "Status" => "Error",
176  "Message" => "HMAC validation failure -- message is corrupted.");
177  }
178 
179  # strip base64 encoding from payload and IV
180  $Payload = base64_decode($PostData["Payload"]);
181  $IV = base64_decode($PostData["IV"]);
182 
183  # decrypt the payload to get the envelope
184  $Env = openssl_decrypt(
185  $Payload, "aes-256-cbc", $EncKey, OPENSSL_RAW_DATA, $IV);
186 
187  # attempt to unserialize the envelope, bailing on failure
188  $Env = json_decode($Env, TRUE);
189  if ($Env === NULL)
190  {
191  return array(
192  "Status" => "Error",
193  "Message" => "Could not decode message envelope.");
194  }
195 
196  # check that the envelope contains all the required headers
197  if (!isset($Env["Version"]) || !isset($Env["Timestamp"]) ||
198  !isset($Env["Cookie"]) || !isset($Env["Data"]) )
199  {
200  return array(
201  "Status" => "Error",
202  "Message" => "Payload did not include all required parameters.");
203  }
204 
205  # check that this is an envelope in a version we understand
206  if ($Env["Version"] != "3")
207  {
208  return array(
209  "Status" => "Error",
210  "Message" => "Message was not version 3.");
211  }
212 
213  # check that this envelope isn't too old
214  if (time() - $Env["Timestamp"] > 300)
215  {
216  return array(
217  "Status" => "Error",
218  "Message" => "Message is more than 5 minutes old.");
219  }
220 
221  # check if this is a duplicate message
222  if (call_user_func($this->CheckForDuplicateFn,
223  $Env["Timestamp"], $Env["Cookie"]))
224  {
225  return array(
226  "Status" => "Error",
227  "Message" => "This is a duplicate message");
228  }
229 
230  call_user_func($this->RegisterMessageFn,
231  $Env["Timestamp"], $Env["Cookie"]);
232 
233  return array(
234  "Status" => "OK",
235  "Data" => $Env["Data"]);
236  }
237 
238  private $APIUrl;
239  private $APIPassword;
240  private $CheckForDuplicateFn;
241  private $RegisterMessageFn;
242 }
EncodeEncryptedMessage($Data)
Construct an encrypted message packet from provided data.
DoRestCommand($Params)
Run a REST API command against a remote site.
__construct($APIUrl, $APIPassword, $CheckForDuplicateFn, $RegisterMessageFn)
Constructor.
DecodeEncryptedMessage($PostData)
Decrypt an encrypted message packet.
This class provides a general-purpose library for encrypted REST calls and responses.