PageRenderTime 30ms CodeModel.GetById 28ms RepoModel.GetById 1ms app.codeStats 0ms

/KMS/libmikey-sakke/selex/mskms/server/mskms.pl

https://bitbucket.org/a30151/mikey-sakke
Perl | 994 lines | 956 code | 32 blank | 6 comment | 7 complexity | 37be9eb5561ef21e76f5a2c01973bb3c MD5 | raw file
Possible License(s): Apache-2.0, BSD-3-Clause, LGPL-2.1, GPL-2.0
  1. #!/usr/bin/site_perl/hypnotoad -f
  2. use lib 'lib';
  3. use File::Spec;
  4. BEGIN
  5. {
  6. if ($ENV{'PAR_TEMP'})
  7. {
  8. my $dir = File::Spec->catfile ($ENV{'PAR_TEMP'}, 'inc');
  9. chdir $dir or die "chdir: '$dir': $!";
  10. }
  11. }
  12. use Mojolicious::Lite;
  13. use IO::Socket::SSL;
  14. use DBI;
  15. use Digest::SHA1 qw/sha1_base64/;
  16. use Data::Dumper;
  17. # https://bitbucket.org/abutcher/crypt-ecdsa-gmpz.git
  18. use Crypt::ECDSA;
  19. use Crypt::ECDSA::Util qw/bint hex_bint as_hex/;
  20. use Math::GMPz qw/Rmpz_invert/;
  21. # for convenience of demonstration a
  22. # SIP registrar and proxy is provided
  23. use Net::SIP;
  24. use Carp qw/croak/;
  25. sub dbconnect { DBI->connect("dbi:SQLite:mskms.db"); }
  26. say "Initializing signing elliptic curve...";
  27. my $sign_ec = Crypt::ECDSA::Curve::Prime->new(standard => 'ECP-256');
  28. say "sign_ec->Gx: ".as_hex($sign_ec->{G_x});
  29. say "sign_ec->Gy: ".as_hex($sign_ec->{G_y});
  30. say "sign_ec->b: ".as_hex($sign_ec->{b});
  31. say "sign_ec->q: ".as_hex($sign_ec->{q});
  32. my %sakke_param_sets;
  33. %sakke_param_sets =
  34. (
  35. 1 => { ## RFC 6509
  36. n => 128,
  37. p => hex_bint (join '',
  38. qw/997ABB1F 0A563FDA 65C61198 DAD0657A
  39. 416C0CE1 9CB48261 BE9AE358 B3E01A2E
  40. F40AAB27 E2FC0F1B 228730D5 31A59CB0
  41. E791B39F F7C88A19 356D27F4 A666A6D0
  42. E26C6487 326B4CD4 512AC5CD 65681CE1
  43. B6AFF4A8 31852A82 A7CF3C52 1C3C09AA
  44. 9F94D6AF 56971F1F FCE3E823 89857DB0
  45. 80C5DF10 AC7ACE87 666D807A FEA85FEB/),
  46. q => hex_bint (join '',
  47. qw/265EAEC7 C2958FF6 99718466 36B4195E
  48. 905B0338 672D2098 6FA6B8D6 2CF8068B
  49. BD02AAC9 F8BF03C6 C8A1CC35 4C69672C
  50. 39E46CE7 FDF22286 4D5B49FD 2999A9B4
  51. 389B1921 CC9AD335 144AB173 595A0738
  52. 6DABFD2A 0C614AA0 A9F3CF14 870F026A
  53. A7E535AB D5A5C7C7 FF38FA08 E2615F6C
  54. 203177C4 2B1EB3A1 D99B601E BFAA17FB/),
  55. Px => hex_bint (join '',
  56. qw/53FC09EE 332C29AD 0A799005 3ED9B52A
  57. 2B1A2FD6 0AEC69C6 98B2F204 B6FF7CBF
  58. B5EDB6C0 F6CE2308 AB10DB90 30B09E10
  59. 43D5F22C DB9DFA55 718BD9E7 406CE890
  60. 9760AF76 5DD5BCCB 337C8654 8B72F2E1
  61. A702C339 7A60DE74 A7C1514D BA66910D
  62. D5CFB4CC 80728D87 EE9163A5 B63F73EC
  63. 80EC46C4 967E0979 880DC8AB EAE63895/),
  64. Py => hex_bint (join '',
  65. qw/0A824906 3F6009F1 F9F1F053 3634A135
  66. D3E82016 02990696 3D778D82 1E141178
  67. F5EA69F4 654EC2B9 E7F7F5E5 F0DE55F6
  68. 6B598CCF 9A140B2E 416CFF0C A9E032B9
  69. 70DAE117 AD547C6C CAD696B5 B7652FE0
  70. AC6F1E80 164AA989 492D979F C5A4D5F2
  71. 13515AD7 E9CB99A9 80BDAD5A D5BB4636
  72. ADB9B570 6A67DCDE 75573FD7 1BEF16D7/),
  73. g => hex_bint (join '',
  74. qw/66FC2A43 2B6EA392 148F1586 7D623068
  75. C6A87BD1 FB94C41E 27FABE65 8E015A87
  76. 371E9474 4C96FEDA 449AE956 3F8BC446
  77. CBFDA85D 5D00EF57 7072DA8F 541721BE
  78. EE0FAED1 828EAB90 B99DFB01 38C78433
  79. 55DF0460 B4A9FD74 B4F1A32B CAFA1FFA
  80. D682C033 A7942BCC E3720F20 B9B7B040
  81. 3C8CAE87 B7A0042A CDE0FAB3 6461EA46/),
  82. sha => 256, # sha-id
  83. hash_len => 32, # octets
  84. }
  85. );
  86. while (my ($id, $set) = each(%sakke_param_sets))
  87. {
  88. $set->{E} = Crypt::ECDSA::Curve::Prime->new
  89. (
  90. standard => 'generic_prime',
  91. p => $set->{p},
  92. a => -3,
  93. b => 0,
  94. );
  95. $set->{P} = Crypt::ECDSA::Point->new
  96. (
  97. curve => $set->{E},
  98. X => $set->{Px},
  99. Y => $set->{Py},
  100. );
  101. die "P is not on curve for parameter set $id"
  102. if not $set->{P}->is_on_curve();
  103. }
  104. # hardcode to param set 1 (6509)
  105. my $sakke_param_set_id = 1;
  106. my $sakke_param_set = $sakke_param_sets{$sakke_param_set_id};
  107. # cache public data
  108. my $this_server_id;
  109. my $Z;
  110. my $KPAK;
  111. if (! -r "mskms.db")
  112. {
  113. say "Creating database...";
  114. my $dbh = dbconnect();
  115. my $sql = <<ENDSQL;
  116. ------------ MIKEY-SAKKE Data ------------
  117. CREATE TABLE server_private_data
  118. (
  119. kmsSecretAuthenticationKey BLOB, -- 6507 integer KSAK.
  120. kmsMasterSecret BLOB -- 6508 integer z.
  121. );
  122. CREATE TABLE server_public_data
  123. (
  124. id INTEGER PRIMARY KEY, -- internal key per community.
  125. kmsIdentifier VARCHAR(255) NOT NULL, -- 6509 arbitrary string.
  126. kmsPublicAuthenticationKey BLOB, -- 6507 ecpoint KPAK.
  127. kmsPublicKey BLOB, -- 6508 ecpoint Z.
  128. sakkeParameterSetIndex INTEGER -- SAKKE parameter set being used.
  129. );
  130. CREATE TABLE identities -- could identify a device,
  131. ( -- access-point/user-agent or user.
  132. userIdentifier BLOB -- user identifier: in 6709 this
  133. PRIMARY KEY, -- includes a datestamp;
  134. accountId INTEGER -- reference to login account
  135. REFERENCES accounts(id), -- that created this data.
  136. userPublicValidationToken BLOB, -- 6507 ecpoint PVT
  137. userSecretSigningKey BLOB, -- 6507 integer SSK
  138. receiverSecretKey BLOB -- 6508 ecpoint RSK
  139. );
  140. CREATE TABLE account_uris
  141. (
  142. id INTEGER REFERENCES accounts(id), -- the account
  143. uri VARCHAR(240) PRIMARY KEY -- a URI registered to the account
  144. );
  145. CREATE INDEX AccountURIs ON account_uris(id);
  146. CREATE INDEX ServerID ON server_public_data(kmsIdentifier);
  147. ------------ User Authentication ------------
  148. CREATE TABLE this_server
  149. (
  150. whoami INTEGER -- ref to public data
  151. REFERENCES server_public_data(id),
  152. dbVersion INTEGER DEFAULT 1 -- db version for migration
  153. );
  154. CREATE TABLE accounts
  155. (
  156. id INTEGER PRIMARY KEY, -- internal key.
  157. name VARCHAR(64) UNIQUE NOT NULL, -- user name for https access.
  158. sha1 CHAR(20), -- SHA-1 of user's password.
  159. admin BOOLEAN -- whether user has access
  160. -- to administrative functions
  161. );
  162. CREATE TABLE auth_sessions
  163. (
  164. sid VARCHAR(255) PRIMARY KEY, -- auth session key
  165. accountId INTEGER -- user authenticated
  166. REFERENCES accounts(id),
  167. lastActivity TIMESTAMP -- last auth activity
  168. DEFAULT CURRENT_TIMESTAMP,
  169. clientData VARCHAR(255) -- user-agent and ip address
  170. );
  171. CREATE INDEX AccountIdentities ON identities(accountId);
  172. CREATE INDEX AccountSessions ON auth_sessions(accountId);
  173. ENDSQL
  174. for my $stm (split /(?<=\));/, $sql)
  175. {
  176. $dbh->do($stm) or die $dbh->errstr . " in " . $stm;
  177. }
  178. print "Creating default users... ";
  179. for (['bob', 'bobpass'],
  180. ['jim', 'jimpass'],
  181. ['admin', 'admin', 1])
  182. {
  183. my ($name, $pass, $admin) = @$_;
  184. $dbh->do(<<ENDSQL,undef,($name, sha1_base64($pass), !!$admin)) or die $dbh->errstr;
  185. INSERT INTO accounts (name, sha1, admin) VALUES (?, ?, ?)
  186. ENDSQL
  187. }
  188. say "Done. Login as admin:admin to reconfigure.";
  189. my $initial_identity = 'unspecified.domain.co.uk';
  190. $dbh->do("INSERT INTO server_public_data (kmsIdentifier, sakkeParameterSetIndex) ".
  191. "VALUES ('$initial_identity', $sakke_param_set_id)") or die $dbh->errstr;
  192. $this_server_id = scalar $dbh->selectrow_array(
  193. "SELECT id FROM server_public_data ".
  194. "WHERE kmsIdentifier = '$initial_identity'") or die $dbh->errstr;
  195. $dbh->do("INSERT INTO this_server (whoami) VALUES ($this_server_id)") or die $dbh->errstr;
  196. say "Configured initial KMS identity as '$initial_identity'. Login as an administrator to reconfigure.";
  197. # Set-up initial MIKEY-SAKKE data
  198. community_rekey($dbh);
  199. $dbh->disconnect();
  200. }
  201. else # cache public data
  202. {
  203. refresh_public_cache();
  204. }
  205. sub refresh_public_cache
  206. {
  207. my $dbh = dbconnect();
  208. $this_server_id = scalar $dbh->selectrow_array('SELECT whoami FROM this_server') or die $dbh->errstr;
  209. $KPAK = Crypt::ECDSA::Point->new(
  210. curve => $sign_ec,
  211. octet => pack('H*',$dbh->selectrow_array(
  212. "SELECT kmsPublicAuthenticationKey ".
  213. "FROM server_public_data ".
  214. "WHERE id = $this_server_id")));
  215. $Z = hex_bint $dbh->selectrow_array("SELECT kmsPublicKey ".
  216. "FROM server_public_data ".
  217. "WHERE id = $this_server_id");
  218. $sakke_param_set_id = $dbh->selectrow_array("SELECT sakkeParameterSetIndex ".
  219. "FROM server_public_data ".
  220. "WHERE id = $this_server_id");
  221. $sakke_param_set = $sakke_param_sets{$sakke_param_set_id};
  222. $dbh->disconnect();
  223. }
  224. sub bitcount
  225. {
  226. my $x = shift;
  227. my $rc = 0;
  228. while ($x != 0)
  229. {
  230. ++$rc;
  231. $x >>= 1;
  232. }
  233. return $rc;
  234. }
  235. sub change_server_name
  236. {
  237. my $dbh = app->dbh;
  238. my $newname = shift;
  239. $dbh->do(<<ENDSQL,undef,($newname)) or croak $dbh->errstr;
  240. UPDATE server_public_data SET kmsIdentifier = ?
  241. WHERE id = $this_server_id
  242. ENDSQL
  243. }
  244. sub is_admin ($)
  245. {
  246. my $accountId_or_mojo = shift;
  247. $accountId_or_mojo = get_account_from_session($accountId_or_mojo) if ref $accountId_or_mojo;
  248. return scalar app->dbh->selectrow_array(
  249. "SELECT admin FROM accounts WHERE id = $accountId_or_mojo");
  250. }
  251. sub json_error
  252. {
  253. ({error => shift}, status => shift // 500);
  254. }
  255. sub add_user
  256. {
  257. my $dbh = app->dbh;
  258. my $accountId = shift;
  259. return json_error "Must be an administrator to add users.", 403 if not is_admin $accountId;
  260. my $name = shift;
  261. my $pass = shift;
  262. my $admin = 0;
  263. $dbh->do(<<ENDSQL,undef,($name, sha1_base64($pass), !!$admin)) or return json_error $dbh->errstr;
  264. INSERT INTO accounts (name, sha1, admin) VALUES (?, ?, ?)
  265. ENDSQL
  266. ({success => "User '$name' added."}, status => 201)
  267. }
  268. sub add_account_uri
  269. {
  270. my $dbh = app->dbh;
  271. my $accountId = shift;
  272. my $uri = shift;
  273. return json_error "No 'uri' parameter specified", 400 if not $uri;
  274. $dbh->do(<<ENDSQL,undef,($accountId, $uri)) or return json_error $dbh->errstr;
  275. INSERT INTO account_uris (id, uri) VALUES (?, ?)
  276. ENDSQL
  277. ({success => "URI '$uri' added to account '$accountId'"}, status => 201)
  278. }
  279. sub update_account_uri
  280. {
  281. my $dbh = app->dbh;
  282. my ($accountId, $from_uri, $to_uri) = @_;
  283. return json_error "No 'uri' query parameter specified", 400 if not $from_uri;
  284. return json_error "No 'uri' field specified in body", 400 if not $to_uri;
  285. return {success => "No change requested."};
  286. my $updated = $dbh->do(<<ENDSQL,undef,($to_uri, $accountId, $from_uri));
  287. UPDATE account_uris SET uri = ? WHERE id = ? AND uri = ?
  288. ENDSQL
  289. return json_error $dbh->errstr if not $updated;
  290. return json_error "URI '$from_uri' not associated with account '$accountId'", 404 if not 0+$updated;
  291. {success => "URI '$from_uri' updated to '$to_uri' for account '$accountId'"}
  292. }
  293. sub remove_account_uri
  294. {
  295. my $dbh = app->dbh;
  296. my $accountId = shift;
  297. my $uri = shift;
  298. my $removed = $dbh->do(<<ENDSQL,undef,($accountId, $uri));
  299. DELETE FROM account_uris
  300. WHERE id = ?
  301. AND uri = ?
  302. ENDSQL
  303. return json_error $dbh->errstr if not $removed;
  304. return json_error "URI '$uri' not associated with account '$accountId'", 404 if not 0+$removed;
  305. {success => "URI '$uri' removed from account '$accountId'"}
  306. }
  307. sub get_KSAK
  308. {
  309. return hex_bint app->dbh->selectrow_array(
  310. "SELECT kmsSecretAuthenticationKey FROM server_private_data");
  311. }
  312. sub get_master_secret
  313. {
  314. return hex_bint app->dbh->selectrow_array(
  315. "SELECT kmsMasterSecret FROM server_private_data");
  316. }
  317. sub get_existing_key_material
  318. {
  319. return json_error "Not implemented", 501
  320. }
  321. sub get_key_material
  322. {
  323. my $dbh = app->dbh;
  324. my $accountId = shift;
  325. my $userIdentifier = shift;
  326. say "get_key_material($accountId, '".($userIdentifier//'')."')";
  327. return json_error "Invalid arguments", 400
  328. if not defined $accountId or not defined $userIdentifier;
  329. # get URI component for matching to user account (commencing after
  330. # first NUL character)
  331. #
  332. my $uri = substr($userIdentifier, index($userIdentifier, "\0")+1);
  333. my $requestedAccountHolder = $dbh->selectrow_array(<<ENDSQL,undef,($uri));
  334. SELECT id FROM account_uris WHERE uri = ?
  335. ENDSQL
  336. return json_error "The URI '$uri' is not associated with an account", 404
  337. if not defined $requestedAccountHolder;
  338. # if the requesting account is the owner of the uri then
  339. # return private data in addition to the public data
  340. #
  341. my $isOwner = $accountId == $requestedAccountHolder;
  342. say "Requesing user: $accountId; Details requested for: $uri: (ID:$userIdentifier) (account:$requestedAccountHolder)";
  343. my $private;
  344. my $error;
  345. if ($isOwner)
  346. {
  347. $private = $dbh->selectrow_hashref(<<ENDSQL,undef,($userIdentifier));
  348. SELECT userSecretSigningKey,
  349. receiverSecretKey
  350. FROM identities
  351. WHERE userIdentifier = ?
  352. ENDSQL
  353. if (not defined($private))
  354. {
  355. # ===== Compute signing keys. =====================
  356. my $AuthKey = Crypt::ECDSA::Key->new(
  357. curve => $sign_ec,
  358. X => $sign_ec->{G_x},
  359. Y => $sign_ec->{G_y},
  360. # d => bint 0x23456, ## XXX:TEST RFC6507 example override
  361. );
  362. my $PVT = $AuthKey->G * $AuthKey->secret;
  363. my $hash = new Digest::SHA($sakke_param_set->{sha});
  364. # XXX:TEST vector from 6507
  365. # $userIdentifier = "2011-02\0tel:+447700900123\0";
  366. say "G := " . unpack('H*', $AuthKey->G->to_octet);
  367. say "KPAK := " . unpack('H*', $KPAK->to_octet);
  368. say "ID := " . $userIdentifier;
  369. say "PVT := " . unpack('H*', $PVT->to_octet);
  370. $hash->add($AuthKey->G->to_octet);
  371. $hash->add($KPAK->to_octet);
  372. $hash->add($userIdentifier);
  373. $hash->add($PVT->to_octet);
  374. my $HS = hex_bint $hash->hexdigest;
  375. my $KSAK = get_KSAK;
  376. my $SSK = ($KSAK + $HS * $AuthKey->secret) % $AuthKey->G->order;
  377. my $SSK_hex = substr(as_hex($SSK),2);
  378. my $PVT_hex = unpack('H*', $PVT->to_octet);
  379. say "$userIdentifier: PVT = " . $PVT_hex;
  380. say "$userIdentifier: HS = " . as_hex($HS);
  381. say "$userIdentifier: SSK = " . $SSK_hex;
  382. # ===== Compute receiver secret. =====================
  383. my $z = get_master_secret;
  384. my $a = hex_bint unpack 'H*', $userIdentifier;
  385. my $RSK = $a + $z;
  386. Rmpz_invert($RSK, $RSK, $sakke_param_set->{q});
  387. $RSK = $sakke_param_set->{P} * $RSK;
  388. my $RSK_hex = unpack('H*', $RSK->to_octet);
  389. say "$userIdentifier: RSK = " . $RSK_hex;
  390. $dbh->do(<<ENDSQL,undef,($userIdentifier, $accountId, $PVT_hex, $SSK_hex, $RSK_hex));
  391. INSERT INTO identities (userIdentifier,
  392. accountId,
  393. userPublicValidationToken,
  394. userSecretSigningKey,
  395. receiverSecretKey)
  396. VALUES (?, ?, ?, ?, ?)
  397. ENDSQL
  398. $error = $dbh->errstr;
  399. $private =
  400. {
  401. userSecretSigningKey => $SSK_hex,
  402. receiverSecretKey => $RSK_hex,
  403. }
  404. if not $error;
  405. }
  406. }
  407. my $public = $dbh->selectrow_hashref(<<ENDSQL,undef,($userIdentifier));
  408. SELECT identities.userPublicValidationToken
  409. FROM identities
  410. WHERE userIdentifier = ?
  411. ENDSQL
  412. my $community = $dbh->selectall_arrayref(<<ENDSQL, {Slice => {}});
  413. SELECT kmsIdentifier,
  414. kmsPublicAuthenticationKey,
  415. kmsPublicKey,
  416. sakkeParameterSetIndex
  417. FROM server_public_data
  418. ENDSQL
  419. my %result;
  420. $result{error} = $error if $error;
  421. $result{public} = $public if $public;
  422. $result{community} = $community if $community;
  423. $result{private} = $private if $private;
  424. return \%result;
  425. }
  426. sub community_rekey
  427. {
  428. my $dbh = shift;
  429. say "Community rekey: Generating new MIKEY-SAKKE secrets and corresponding public data...";
  430. my $AuthKey = Crypt::ECDSA::Key->new(
  431. curve => $sign_ec,
  432. X => $sign_ec->{G_x},
  433. Y => $sign_ec->{G_y},
  434. # d => bint 0x12345, ## XXX:TEST RFC6507 example override
  435. );
  436. my $KSAK = $AuthKey->secret;
  437. $KPAK = $AuthKey->Q; # cache in global
  438. my $q = $sakke_param_set->{q};
  439. my $z;
  440. {
  441. my $qw = bitcount($q-1);
  442. say $qw;
  443. for (;;) # probably unnecessary
  444. {
  445. my $zr = Crypt::ECDSA::Util::random_bits($qw);
  446. $z = $zr % $q;
  447. last # if $zr != $z;
  448. }
  449. }
  450. # XXX:TEST overwrite random with RFC6508 example
  451. # $z = hex_bint join '', qw/AFF429D3 5F84B110 D094803B 3595A6E2 998BC99F/;
  452. # cache in global
  453. $Z = $sakke_param_set->{P} * $z;
  454. open KEYLOG, ">>keys.log";
  455. for my $file (\*STDERR, \*KEYLOG) {
  456. say $file "Authentication Keys...";
  457. say $file "G: ".unpack('H*', $AuthKey->G->to_octet);
  458. say $file "KSAK: ".as_hex($KSAK);
  459. say $file "KPAK: ".unpack('H*', $KPAK->to_octet);
  460. say $file "CHECK: ".unpack('H*', ($AuthKey->G * $AuthKey->secret)->to_octet);
  461. say $file "SAKKE Keys...";
  462. say $file "n: ".$sakke_param_set->{n};
  463. say $file "p: ".as_hex($sakke_param_set->{p});
  464. say $file "q: ".as_hex($sakke_param_set->{q});
  465. say $file "Px: ".as_hex($sakke_param_set->{P}->X);
  466. say $file "Py: ".as_hex($sakke_param_set->{P}->Y);
  467. say $file "P->order: ".as_hex($sakke_param_set->{P}->order);
  468. say $file "g: ".as_hex($sakke_param_set->{g});
  469. say $file "sha: ".$sakke_param_set->{sha};
  470. say $file "hash_len: ".$sakke_param_set->{hash_len};
  471. say $file "z: ".as_hex($z);
  472. say $file "Z: ".unpack('H*', $Z->to_octet);
  473. }
  474. close KEYLOG;
  475. $dbh->do('DELETE FROM identities');
  476. $dbh->do('DELETE FROM server_private_data');
  477. $dbh->do(<<ENDSQL,undef,(substr(as_hex($KSAK),2), substr(as_hex($z),2)));
  478. INSERT INTO server_private_data VALUES (?, ?);
  479. ENDSQL
  480. $dbh->do(<<ENDSQL,undef,(unpack('H*', $KPAK->to_octet), unpack('H*', $Z->to_octet)))
  481. UPDATE server_public_data
  482. SET kmsPublicAuthenticationKey = ?,
  483. kmsPublicKey = ?
  484. WHERE id = $this_server_id
  485. ENDSQL
  486. }
  487. sub get_account_from_session
  488. {
  489. my $mojo = shift;
  490. my $sid = $mojo->session('sid');
  491. return scalar app->dbh->selectrow_array(<<ENDSQL,undef,($sid));
  492. SELECT accountId FROM auth_sessions WHERE sid = ?
  493. ENDSQL
  494. }
  495. sub resolve_to_account
  496. {
  497. my $name_or_uid = shift;
  498. return scalar app->dbh->selectrow_array(<<ENDSQL,undef,($name_or_uid));
  499. SELECT id FROM accounts WHERE name = :1 OR id = :1
  500. ENDSQL
  501. }
  502. # set mojo default listen address if not running under hypnotoad (i.e.
  503. # running on windows or standalone)
  504. $ENV{MOJO_LISTEN} = 'https://*:7070' if not $ENV{MOJO_LISTEN};
  505. app->attr(dbh => \&dbconnect);
  506. app->config(hypnotoad => {listen => ['https://*:7070']});
  507. plugin 'auth_helper';
  508. plugin 'basic_auth';
  509. # set to 1 to terminate the SIP thread
  510. my $sip_term = 0;
  511. my $use_net_interface = $^O !~ /MSWin/;
  512. my $get_local_ip_addresses;
  513. sub get_local_ip_addresses { $get_local_ip_addresses->(@_); }
  514. if ($use_net_interface) # posix-ish
  515. {
  516. eval "use Net::Interface qw/inet_ntoa/";
  517. $get_local_ip_addresses = sub
  518. {
  519. my @ips = ();
  520. my $opts = shift;
  521. my $exclude_loopback = not ($opts && $opts->{include_loopback});
  522. for my $if (Net::Interface->interfaces)
  523. {
  524. if (my $raw_addr = $if->address())
  525. {
  526. my $addr = inet_ntoa($raw_addr);
  527. next if $exclude_loopback and ($addr =~ m/^127/ or $addr eq '::1');
  528. push @ips, $addr;
  529. }
  530. }
  531. return @ips;
  532. }
  533. }
  534. else # use gethostbyname
  535. {
  536. $get_local_ip_addresses = sub
  537. {
  538. my @ips = ();
  539. my $opts = shift;
  540. my ($name, $aliases, $addrtype, $length, @addrs) = gethostbyname('localhost');
  541. if ($opts && $opts->{include_loopback})
  542. {
  543. foreach my $addr (@addrs)
  544. {
  545. push @ips, join('.', unpack('C4', $addr));
  546. }
  547. }
  548. ($name, $aliases, $addrtype, $length, @addrs) = gethostbyname($name);
  549. foreach my $addr (@addrs)
  550. {
  551. unshift @ips, join('.', unpack('C4', $addr));
  552. }
  553. return @ips;
  554. }
  555. }
  556. sub render_json_error
  557. {
  558. my $mojo = shift;
  559. $mojo->render_json(json_error @_);
  560. }
  561. sub render_page
  562. {
  563. my $mojo = shift;
  564. my $page = shift;
  565. my $accountId = get_account_from_session($mojo);
  566. my ($user, $admin) = app->dbh->selectrow_array(
  567. "SELECT name, admin FROM accounts WHERE id = $accountId");
  568. return $mojo->render($page, (uid => $accountId, user => $user, admin => $admin));
  569. }
  570. # under '/' => sub
  571. # {
  572. # my $mojo = shift;
  573. # say $mojo->req->method . ' ' . $mojo->req->url;
  574. # print Dumper($mojo->session);
  575. # };
  576. group
  577. {
  578. under '/secure' => sub
  579. {
  580. my $mojo = shift;
  581. return 1 if $mojo->check_auth()->{authorized};
  582. # support direct access via basic-auth for /key subpath
  583. if ((my $sub = $mojo->req->url->path->parts->[1]))
  584. {
  585. if ($sub eq 'key')
  586. {
  587. return $mojo->basic_auth('mikey-sakke-kms' => sub
  588. {
  589. if (not $mojo->login(@_)->{authorized})
  590. {
  591. $mojo->res->headers->www_authenticate("Basic realm=mikey-sakke-kms");
  592. $mojo->res->code(401);
  593. $mojo->rendered;
  594. return 0;
  595. }
  596. return 1;
  597. });
  598. }
  599. elsif ($sub eq '1') # REST API 1 supports session creation via GET /secure/1/session
  600. {
  601. if ($mojo->req->url->path->parts->[2] eq 'session')
  602. {
  603. return 1 if $mojo->req->method eq 'GET';
  604. render_json_error $mojo, "Not authenticated, no session to delete.", 401;
  605. return 0;
  606. }
  607. }
  608. }
  609. $mojo->session('post-login-url' => $mojo->req->url->path->to_abs_string);
  610. $mojo->flash(error => "401:Authentication required.");
  611. $mojo->redirect_to('/login'); # re-direct to here when not logged in.
  612. return 0;
  613. };
  614. get '/' => sub
  615. {
  616. render_page shift, 'main';
  617. };
  618. group { under '1'; ## version 1 REST API group
  619. get '/session' => sub
  620. {
  621. my $mojo = shift;
  622. my $res = $mojo->login($mojo->param('username'), $mojo->param('password'));
  623. $mojo->res->code(401) if not $res->{authorized};
  624. return $mojo->render_json($res);
  625. };
  626. del '/session' => sub
  627. {
  628. my $mojo = shift;
  629. $mojo->logout($mojo->param('all-sessions'));
  630. return $mojo->rendered(205);
  631. };
  632. any [qw(POST GET)] => '/rekey' => sub
  633. {
  634. my $mojo = shift;
  635. return render_json_error $mojo, "Must be an administrator to purge keys.", 403 if not is_admin $mojo;
  636. community_rekey app->dbh;
  637. return $mojo->render_json({success => 'All user keys revoked and community keys recreated.'})
  638. };
  639. post '/purge/:sakkeid' => sub
  640. {
  641. my $mojo = shift;
  642. my $sakkeid = $mojo->param('sakkeid');
  643. my $auth = $mojo->req->json('/auth');
  644. my $identity = app->dbh->selectrow_array(<<ENDSQL,undef,($sakkeid));
  645. SELECT userSecretSigningKey,
  646. receiverSecretKey
  647. FROM identities
  648. WHERE userIdentifier = ?
  649. ENDSQL
  650. return render_json_error $mojo, "No such identity.", 404 if not @$identity;
  651. my $ssk = hex_bint $identity->[0];
  652. my $rsk = hex_bint $identity->[1];
  653. my $xor = (hex_bint $auth) % $ssk;
  654. say "Purge '$sakkeid': SSK: $identity->[0]";
  655. say "Purge '$sakkeid': RSK: $identity->[1]";
  656. say "Purge '$sakkeid': AUTH: $auth";
  657. say "Purge '$sakkeid': XOR: ".as_hex($xor);
  658. return render_json_error $mojo, "Refusing to purge keys for '$sakkeid', authentication failed..", 403 if $xor ne $rsk;
  659. app->dbh->do(<<ENDSQL,undef,($sakkeid));
  660. DELETE FROM identities
  661. WHERE userIdentifier = ?
  662. ENDSQL
  663. say "Purged '$sakkeid' keys. User agent deemed them invalid.";
  664. open KEYLOG, ">>keys.log";
  665. say KEYLOG "-----Invalid keys for $sakkeid------";
  666. say KEYLOG "SSK: $identity->[0]";
  667. say KEYLOG "RSK: $identity->[1]";
  668. say KEYLOG "AUTH: $auth";
  669. say KEYLOG "XOR: ".as_hex($xor);
  670. close KEYLOG;
  671. return $mojo->render_json({success => "Keys revoked for '$sakkeid'."})
  672. };
  673. get '/user' => sub
  674. {
  675. my $mojo = shift;
  676. my $accountId = get_account_from_session($mojo);
  677. my $where_clause = '';
  678. $where_clause = "WHERE id = $accountId" if $mojo->param('restrict');
  679. my $users = app->dbh->selectall_arrayref(
  680. "SELECT id, name FROM accounts $where_clause", {Slice => {}});
  681. return $mojo->render_json($users);
  682. };
  683. post '/user' => sub
  684. {
  685. my $mojo = shift;
  686. my $accountId = get_account_from_session($mojo);
  687. my $name = $mojo->req->json('/name');
  688. my $pass = $mojo->req->json('/pass');
  689. return render_json_error $mojo, "Both 'user' and 'pass' must be provided and non-empty.", 400
  690. if not $name or not $pass;
  691. return $mojo->render_json(add_user($accountId, $name, $pass));
  692. };
  693. get '/user/:name_or_uid/keys' => sub
  694. {
  695. my $mojo = shift;
  696. my $requestingAccountId = get_account_from_session($mojo);
  697. my $subjectAccountId = resolve_to_account($mojo->param('name_or_uid'));
  698. return render_json_error $mojo, "Only the account owner or an administrator may enumerate key material.", 403
  699. if $subjectAccountId ne $requestingAccountId and not is_admin $requestingAccountId;
  700. return $mojo->render_json(get_existing_key_material($requestingAccountId, $subjectAccountId));
  701. };
  702. get '/keys' => sub
  703. {
  704. my $mojo = shift;
  705. my $accountId = get_account_from_session($mojo);
  706. my $userIdentifier = $mojo->param('id');
  707. return $mojo->render_json(get_key_material($accountId, $userIdentifier));
  708. };
  709. any [qw(POST GET PUT DELETE)] => '/user/:name_or_uid/uri' => sub
  710. {
  711. my $mojo = shift;
  712. my $name_or_uid = $mojo->param('name_or_uid');
  713. my $subjectId = resolve_to_account($name_or_uid);
  714. return render_json_error $mojo, "No such account '$name_or_uid'", 404
  715. if not defined $subjectId;
  716. my $method = $mojo->req->method;
  717. if ($method eq 'GET')
  718. {
  719. my $uris = app->dbh->selectall_arrayref(
  720. "SELECT uri FROM account_uris WHERE id = $subjectId", {Slice => {}}) ;
  721. return $mojo->render_json($uris);
  722. }
  723. my $accountId = get_account_from_session($mojo);
  724. return render_json_error $mojo, "Only the account owner or an administrator may add manage URIs.", 403
  725. if $subjectId ne $accountId and not is_admin $accountId;
  726. my $body_uri = $mojo->req->json('/uri');
  727. my $query_uri = $mojo->req->param('uri');
  728. return $mojo->render_json(add_account_uri $subjectId, $body_uri) if $method eq 'POST';
  729. return $mojo->render_json(update_account_uri $subjectId, $query_uri, $body_uri) if $method eq 'PUT';
  730. return $mojo->render_json(remove_account_uri $subjectId, $query_uri) if $method eq 'DELETE';
  731. };
  732. }; # version 1 REST API group
  733. # backward compatibility for previous clients
  734. get '/key' => sub # FIXME: this duplicates '1/keys' rather than redirecting in order to avoid 302 redirect response; ideally want to reuse (or alias)
  735. {
  736. my $mojo = shift;
  737. my $accountId = get_account_from_session($mojo);
  738. my $userIdentifier = $mojo->param('id');
  739. return $mojo->render_json(get_key_material($accountId, $userIdentifier));
  740. };
  741. };
  742. get '/login' => sub
  743. {
  744. my $mojo = shift;
  745. my $error = $mojo->flash('error');
  746. if ($error and $error =~ m/^([0-9]+):(.*)$/)
  747. {
  748. $mojo->res->code($1);
  749. $error = $2;
  750. }
  751. $mojo->render('main', (login => 1, error => $error));
  752. };
  753. post '/login' => sub
  754. {
  755. my $mojo = shift;
  756. my $user = $mojo->login($mojo->param('username'), $mojo->param('password'));
  757. if ($user->{authorized})
  758. {
  759. my $post_login_url = $mojo->session('post-login-url') || '/';
  760. delete $mojo->session->{'post-login-url'};
  761. return $mojo->redirect_to($post_login_url);
  762. }
  763. $mojo->flash(error => "401:Failed to authenticate.");
  764. $mojo->redirect_to('/login');
  765. };
  766. any ['get', 'post'] => '/logout' => sub
  767. {
  768. my $mojo = shift;
  769. $mojo->logout($mojo->param('all-sessions'));
  770. $mojo->redirect_to('/');
  771. };
  772. get '/ui/:page' => sub
  773. {
  774. my $mojo = shift;
  775. render_page $mojo, $mojo->param('page');
  776. };
  777. get '/' => sub
  778. {
  779. my $mojo = shift;
  780. return $mojo->redirect_to('/login') if not $mojo->check_auth()->{authorized};
  781. return $mojo->redirect_to('/secure/')
  782. };
  783. Net::SIP::Debug->level(scalar grep /^-d$/, @ARGV);
  784. # any arguments that look like IP addresses are considered explicit bind addresses for the SIP endpoint
  785. my @bind_addresses = grep /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/, @ARGV;
  786. @bind_addresses = get_local_ip_addresses {include_loopback => 0}
  787. if not @bind_addresses;
  788. # start SIP server
  789. use threads;
  790. my $sip_thread = threads->create(sub {
  791. my @legs = ();
  792. for my $addr (@bind_addresses)
  793. {
  794. push @legs, "udp:$addr:5060";
  795. }
  796. my $sip = Net::SIP::Simple->new( legs => \@legs );
  797. $sip->create_chain([
  798. $sip->create_registrar(min_expires => 30, max_expires => 3600),
  799. $sip->create_stateless_proxy,
  800. ]);
  801. say 'SIP endpoint listening on UDP/5060 ('.join(', ', @bind_addresses).")";
  802. $sip->loop( undef, \$sip_term );
  803. say 'SIP endpoint stopped.';
  804. });
  805. # start HTTPS server
  806. app->start;
  807. # vim: ft=perlsql