Page MenuHomePhorge

No OneTemporary

diff --git a/resources/sql/autopatches/20181213.auth.04.longerhashes.sql b/resources/sql/autopatches/20181213.auth.04.longerhashes.sql
new file mode 100644
index 0000000000..2bffb4c4a8
--- /dev/null
+++ b/resources/sql/autopatches/20181213.auth.04.longerhashes.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_user.phabricator_session
+ CHANGE sessionKey sessionKey VARBINARY(64) NOT NULL;
diff --git a/resources/sql/autopatches/20181213.auth.05.longerloghashes.sql b/resources/sql/autopatches/20181213.auth.05.longerloghashes.sql
new file mode 100644
index 0000000000..dc8638d91c
--- /dev/null
+++ b/resources/sql/autopatches/20181213.auth.05.longerloghashes.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_user.user_log
+ CHANGE session session VARBINARY(64);
diff --git a/resources/sql/patches/20130530.sessionhash.php b/resources/sql/patches/20130530.sessionhash.php
index 771dac61e3..1e09ee32fd 100644
--- a/resources/sql/patches/20130530.sessionhash.php
+++ b/resources/sql/patches/20130530.sessionhash.php
@@ -1,22 +1,7 @@
<?php
-$table = new PhabricatorUser();
-$table->openTransaction();
-$conn = $table->establishConnection('w');
-
-$sessions = queryfx_all(
- $conn,
- 'SELECT userPHID, type, sessionKey FROM %T FOR UPDATE',
- PhabricatorUser::SESSION_TABLE);
-
-foreach ($sessions as $session) {
- queryfx(
- $conn,
- 'UPDATE %T SET sessionKey = %s WHERE userPHID = %s AND type = %s',
- PhabricatorUser::SESSION_TABLE,
- PhabricatorHash::weakDigest($session['sessionKey']),
- $session['userPHID'],
- $session['type']);
-}
-
-$table->saveTransaction();
+// See T13225. Long ago, this upgraded session key storage from unhashed to
+// HMAC-SHA1 here. We later upgraded storage to HMAC-SHA256, so this is initial
+// upgrade is now fairly pointless. Dropping this migration entirely only logs
+// users out of installs that waited more than 5 years to upgrade, which seems
+// like a reasonable behavior.
diff --git a/src/applications/auth/controller/PhabricatorAuthTerminateSessionController.php b/src/applications/auth/controller/PhabricatorAuthTerminateSessionController.php
index fa58977c90..413d0306b9 100644
--- a/src/applications/auth/controller/PhabricatorAuthTerminateSessionController.php
+++ b/src/applications/auth/controller/PhabricatorAuthTerminateSessionController.php
@@ -1,77 +1,78 @@
<?php
final class PhabricatorAuthTerminateSessionController
extends PhabricatorAuthController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$id = $request->getURIData('id');
$is_all = ($id === 'all');
$query = id(new PhabricatorAuthSessionQuery())
->setViewer($viewer)
->withIdentityPHIDs(array($viewer->getPHID()));
if (!$is_all) {
$query->withIDs(array($id));
}
- $current_key = PhabricatorHash::weakDigest(
- $request->getCookie(PhabricatorCookies::COOKIE_SESSION));
+ $current_key = PhabricatorAuthSession::newSessionDigest(
+ new PhutilOpaqueEnvelope(
+ $request->getCookie(PhabricatorCookies::COOKIE_SESSION)));
$sessions = $query->execute();
foreach ($sessions as $key => $session) {
$is_current = phutil_hashes_are_identical(
$session->getSessionKey(),
$current_key);
if ($is_current) {
// Don't terminate the current login session.
unset($sessions[$key]);
}
}
$panel_uri = '/settings/panel/sessions/';
if (!$sessions) {
return $this->newDialog()
->setTitle(pht('No Matching Sessions'))
->appendParagraph(
pht('There are no matching sessions to terminate.'))
->appendParagraph(
pht(
'(You can not terminate your current login session. To '.
'terminate it, log out.)'))
->addCancelButton($panel_uri);
}
if ($request->isDialogFormPost()) {
foreach ($sessions as $session) {
$session->delete();
}
return id(new AphrontRedirectResponse())->setURI($panel_uri);
}
if ($is_all) {
$title = pht('Terminate Sessions?');
$short = pht('Terminate Sessions');
$body = pht(
'Really terminate all sessions? (Your current login session will '.
'not be terminated.)');
} else {
$title = pht('Terminate Session?');
$short = pht('Terminate Session');
$body = pht(
'Really terminate session %s?',
phutil_tag('strong', array(), substr($session->getSessionKey(), 0, 6)));
}
return $this->newDialog()
->setTitle($title)
->setShortTitle($short)
->appendParagraph($body)
->addSubmitButton(pht('Terminate'))
->addCancelButton($panel_uri);
}
}
diff --git a/src/applications/auth/controller/PhabricatorAuthUnlinkController.php b/src/applications/auth/controller/PhabricatorAuthUnlinkController.php
index 6211e78110..e6e1493e5a 100644
--- a/src/applications/auth/controller/PhabricatorAuthUnlinkController.php
+++ b/src/applications/auth/controller/PhabricatorAuthUnlinkController.php
@@ -1,145 +1,146 @@
<?php
final class PhabricatorAuthUnlinkController
extends PhabricatorAuthController {
private $providerKey;
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$this->providerKey = $request->getURIData('pkey');
list($type, $domain) = explode(':', $this->providerKey, 2);
// Check that this account link actually exists. We don't require the
// provider to exist because we want users to be able to delete links to
// dead accounts if they want.
$account = id(new PhabricatorExternalAccount())->loadOneWhere(
'accountType = %s AND accountDomain = %s AND userPHID = %s',
$type,
$domain,
$viewer->getPHID());
if (!$account) {
return $this->renderNoAccountErrorDialog();
}
// Check that the provider (if it exists) allows accounts to be unlinked.
$provider_key = $this->providerKey;
$provider = PhabricatorAuthProvider::getEnabledProviderByKey($provider_key);
if ($provider) {
if (!$provider->shouldAllowAccountUnlink()) {
return $this->renderNotUnlinkableErrorDialog($provider);
}
}
// Check that this account isn't the last account which can be used to
// login. We prevent you from removing the last account.
if ($account->isUsableForLogin()) {
$other_accounts = id(new PhabricatorExternalAccount())->loadAllWhere(
'userPHID = %s',
$viewer->getPHID());
$valid_accounts = 0;
foreach ($other_accounts as $other_account) {
if ($other_account->isUsableForLogin()) {
$valid_accounts++;
}
}
if ($valid_accounts < 2) {
return $this->renderLastUsableAccountErrorDialog();
}
}
if ($request->isDialogFormPost()) {
$account->delete();
id(new PhabricatorAuthSessionEngine())->terminateLoginSessions(
$viewer,
- $request->getCookie(PhabricatorCookies::COOKIE_SESSION));
+ new PhutilOpaqueEnvelope(
+ $request->getCookie(PhabricatorCookies::COOKIE_SESSION)));
return id(new AphrontRedirectResponse())->setURI($this->getDoneURI());
}
return $this->renderConfirmDialog();
}
private function getDoneURI() {
return '/settings/panel/external/';
}
private function renderNoAccountErrorDialog() {
$dialog = id(new AphrontDialogView())
->setUser($this->getRequest()->getUser())
->setTitle(pht('No Such Account'))
->appendChild(
pht(
'You can not unlink this account because it is not linked.'))
->addCancelButton($this->getDoneURI());
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function renderNotUnlinkableErrorDialog(
PhabricatorAuthProvider $provider) {
$dialog = id(new AphrontDialogView())
->setUser($this->getRequest()->getUser())
->setTitle(pht('Permanent Account Link'))
->appendChild(
pht(
'You can not unlink this account because the administrator has '.
'configured Phabricator to make links to %s accounts permanent.',
$provider->getProviderName()))
->addCancelButton($this->getDoneURI());
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function renderLastUsableAccountErrorDialog() {
$dialog = id(new AphrontDialogView())
->setUser($this->getRequest()->getUser())
->setTitle(pht('Last Valid Account'))
->appendChild(
pht(
'You can not unlink this account because you have no other '.
'valid login accounts. If you removed it, you would be unable '.
'to log in. Add another authentication method before removing '.
'this one.'))
->addCancelButton($this->getDoneURI());
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function renderConfirmDialog() {
$provider_key = $this->providerKey;
$provider = PhabricatorAuthProvider::getEnabledProviderByKey($provider_key);
if ($provider) {
$title = pht('Unlink "%s" Account?', $provider->getProviderName());
$body = pht(
'You will no longer be able to use your %s account to '.
'log in to Phabricator.',
$provider->getProviderName());
} else {
$title = pht('Unlink Account?');
$body = pht(
'You will no longer be able to use this account to log in '.
'to Phabricator.');
}
$dialog = id(new AphrontDialogView())
->setUser($this->getRequest()->getUser())
->setTitle($title)
->appendParagraph($body)
->appendParagraph(
pht(
'Note: Unlinking an authentication provider will terminate any '.
'other active login sessions.'))
->addSubmitButton(pht('Unlink Account'))
->addCancelButton($this->getDoneURI());
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php
index ada4162067..2020e4a542 100644
--- a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php
+++ b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php
@@ -1,913 +1,940 @@
<?php
/**
*
* @task use Using Sessions
* @task new Creating Sessions
* @task hisec High Security
* @task partial Partial Sessions
* @task onetime One Time Login URIs
* @task cache User Cache
*/
final class PhabricatorAuthSessionEngine extends Phobject {
/**
* Session issued to normal users after they login through a standard channel.
* Associates the client with a standard user identity.
*/
const KIND_USER = 'U';
/**
* Session issued to users who login with some sort of credentials but do not
* have full accounts. These are sometimes called "grey users".
*
* TODO: We do not currently issue these sessions, see T4310.
*/
const KIND_EXTERNAL = 'X';
/**
* Session issued to logged-out users which has no real identity information.
* Its purpose is to protect logged-out users from CSRF.
*/
const KIND_ANONYMOUS = 'A';
/**
* Session kind isn't known.
*/
const KIND_UNKNOWN = '?';
const ONETIME_RECOVER = 'recover';
const ONETIME_RESET = 'reset';
const ONETIME_WELCOME = 'welcome';
const ONETIME_USERNAME = 'rename';
/**
* Get the session kind (e.g., anonymous, user, external account) from a
* session token. Returns a `KIND_` constant.
*
* @param string Session token.
* @return const Session kind constant.
*/
public static function getSessionKindFromToken($session_token) {
if (strpos($session_token, '/') === false) {
// Old-style session, these are all user sessions.
return self::KIND_USER;
}
list($kind, $key) = explode('/', $session_token, 2);
switch ($kind) {
case self::KIND_ANONYMOUS:
case self::KIND_USER:
case self::KIND_EXTERNAL:
return $kind;
default:
return self::KIND_UNKNOWN;
}
}
/**
* Load the user identity associated with a session of a given type,
* identified by token.
*
* When the user presents a session token to an API, this method verifies
* it is of the correct type and loads the corresponding identity if the
* session exists and is valid.
*
* NOTE: `$session_type` is the type of session that is required by the
* loading context. This prevents use of a Conduit sesssion as a Web
* session, for example.
*
* @param const The type of session to load.
* @param string The session token.
* @return PhabricatorUser|null
* @task use
*/
public function loadUserForSession($session_type, $session_token) {
$session_kind = self::getSessionKindFromToken($session_token);
switch ($session_kind) {
case self::KIND_ANONYMOUS:
// Don't bother trying to load a user for an anonymous session, since
// neither the session nor the user exist.
return null;
case self::KIND_UNKNOWN:
// If we don't know what kind of session this is, don't go looking for
// it.
return null;
case self::KIND_USER:
break;
case self::KIND_EXTERNAL:
// TODO: Implement these (T4310).
return null;
}
$session_table = new PhabricatorAuthSession();
$user_table = new PhabricatorUser();
- $conn_r = $session_table->establishConnection('r');
- $session_key = PhabricatorHash::weakDigest($session_token);
+ $conn = $session_table->establishConnection('r');
- $cache_parts = $this->getUserCacheQueryParts($conn_r);
+ // TODO: See T13225. We're moving sessions to a more modern digest
+ // algorithm, but still accept older cookies for compatibility.
+ $session_key = PhabricatorAuthSession::newSessionDigest(
+ new PhutilOpaqueEnvelope($session_token));
+ $weak_key = PhabricatorHash::weakDigest($session_token);
+
+ $cache_parts = $this->getUserCacheQueryParts($conn);
list($cache_selects, $cache_joins, $cache_map, $types_map) = $cache_parts;
$info = queryfx_one(
- $conn_r,
+ $conn,
'SELECT
s.id AS s_id,
s.phid AS s_phid,
s.sessionExpires AS s_sessionExpires,
s.sessionStart AS s_sessionStart,
s.highSecurityUntil AS s_highSecurityUntil,
s.isPartial AS s_isPartial,
s.signedLegalpadDocuments as s_signedLegalpadDocuments,
+ IF(s.sessionKey = %P, 1, 0) as s_weak,
u.*
%Q
- FROM %T u JOIN %T s ON u.phid = s.userPHID
- AND s.type = %s AND s.sessionKey = %P %Q',
+ FROM %R u JOIN %R s ON u.phid = s.userPHID
+ AND s.type = %s AND s.sessionKey IN (%P, %P) %Q',
+ new PhutilOpaqueEnvelope($weak_key),
$cache_selects,
- $user_table->getTableName(),
- $session_table->getTableName(),
+ $user_table,
+ $session_table,
$session_type,
new PhutilOpaqueEnvelope($session_key),
+ new PhutilOpaqueEnvelope($weak_key),
$cache_joins);
if (!$info) {
return null;
}
+ // TODO: Remove this, see T13225.
+ $is_weak = (bool)$info['s_weak'];
+ unset($info['s_weak']);
+
$session_dict = array(
'userPHID' => $info['phid'],
'sessionKey' => $session_key,
'type' => $session_type,
);
$cache_raw = array_fill_keys($cache_map, null);
foreach ($info as $key => $value) {
if (strncmp($key, 's_', 2) === 0) {
unset($info[$key]);
$session_dict[substr($key, 2)] = $value;
continue;
}
if (isset($cache_map[$key])) {
unset($info[$key]);
$cache_raw[$cache_map[$key]] = $value;
continue;
}
}
$user = $user_table->loadFromArray($info);
$cache_raw = $this->filterRawCacheData($user, $types_map, $cache_raw);
$user->attachRawCacheData($cache_raw);
switch ($session_type) {
case PhabricatorAuthSession::TYPE_WEB:
// Explicitly prevent bots and mailing lists from establishing web
// sessions. It's normally impossible to attach authentication to these
// accounts, and likewise impossible to generate sessions, but it's
// technically possible that a session could exist in the database. If
// one does somehow, refuse to load it.
if (!$user->canEstablishWebSessions()) {
return null;
}
break;
}
$session = id(new PhabricatorAuthSession())->loadFromArray($session_dict);
$ttl = PhabricatorAuthSession::getSessionTypeTTL($session_type);
// If more than 20% of the time on this session has been used, refresh the
// TTL back up to the full duration. The idea here is that sessions are
// good forever if used regularly, but get GC'd when they fall out of use.
// NOTE: If we begin rotating session keys when extending sessions, the
// CSRF code needs to be updated so CSRF tokens survive session rotation.
if (time() + (0.80 * $ttl) > $session->getSessionExpires()) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$conn_w = $session_table->establishConnection('w');
queryfx(
$conn_w,
'UPDATE %T SET sessionExpires = UNIX_TIMESTAMP() + %d WHERE id = %d',
$session->getTableName(),
$ttl,
$session->getID());
unset($unguarded);
}
+ // TODO: Remove this, see T13225.
+ if ($is_weak) {
+ $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
+ $conn_w = $session_table->establishConnection('w');
+ queryfx(
+ $conn_w,
+ 'UPDATE %T SET sessionKey = %P WHERE id = %d',
+ $session->getTableName(),
+ new PhutilOpaqueEnvelope($session_key),
+ $session->getID());
+ unset($unguarded);
+ }
+
$user->attachSession($session);
return $user;
}
/**
* Issue a new session key for a given identity. Phabricator supports
* different types of sessions (like "web" and "conduit") and each session
* type may have multiple concurrent sessions (this allows a user to be
* logged in on multiple browsers at the same time, for instance).
*
* Note that this method is transport-agnostic and does not set cookies or
* issue other types of tokens, it ONLY generates a new session key.
*
* You can configure the maximum number of concurrent sessions for various
* session types in the Phabricator configuration.
*
* @param const Session type constant (see
* @{class:PhabricatorAuthSession}).
* @param phid|null Identity to establish a session for, usually a user
* PHID. With `null`, generates an anonymous session.
* @param bool True to issue a partial session.
* @return string Newly generated session key.
*/
public function establishSession($session_type, $identity_phid, $partial) {
// Consume entropy to generate a new session key, forestalling the eventual
// heat death of the universe.
$session_key = Filesystem::readRandomCharacters(40);
if ($identity_phid === null) {
return self::KIND_ANONYMOUS.'/'.$session_key;
}
$session_table = new PhabricatorAuthSession();
$conn_w = $session_table->establishConnection('w');
// This has a side effect of validating the session type.
$session_ttl = PhabricatorAuthSession::getSessionTypeTTL($session_type);
- $digest_key = PhabricatorHash::weakDigest($session_key);
+ $digest_key = PhabricatorAuthSession::newSessionDigest(
+ new PhutilOpaqueEnvelope($session_key));
// Logging-in users don't have CSRF stuff yet, so we have to unguard this
// write.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
id(new PhabricatorAuthSession())
->setUserPHID($identity_phid)
->setType($session_type)
->setSessionKey($digest_key)
->setSessionStart(time())
->setSessionExpires(time() + $session_ttl)
->setIsPartial($partial ? 1 : 0)
->setSignedLegalpadDocuments(0)
->save();
$log = PhabricatorUserLog::initializeNewLog(
null,
$identity_phid,
($partial
? PhabricatorUserLog::ACTION_LOGIN_PARTIAL
: PhabricatorUserLog::ACTION_LOGIN));
$log->setDetails(
array(
'session_type' => $session_type,
));
$log->setSession($digest_key);
$log->save();
unset($unguarded);
$info = id(new PhabricatorAuthSessionInfo())
->setSessionType($session_type)
->setIdentityPHID($identity_phid)
->setIsPartial($partial);
$extensions = PhabricatorAuthSessionEngineExtension::getAllExtensions();
foreach ($extensions as $extension) {
$extension->didEstablishSession($info);
}
return $session_key;
}
/**
* Terminate all of a user's login sessions.
*
* This is used when users change passwords, linked accounts, or add
* multifactor authentication.
*
* @param PhabricatorUser User whose sessions should be terminated.
* @param string|null Optionally, one session to keep. Normally, the current
* login session.
*
* @return void
*/
public function terminateLoginSessions(
PhabricatorUser $user,
- $except_session = null) {
+ PhutilOpaqueEnvelope $except_session = null) {
$sessions = id(new PhabricatorAuthSessionQuery())
->setViewer($user)
->withIdentityPHIDs(array($user->getPHID()))
->execute();
if ($except_session !== null) {
- $except_session = PhabricatorHash::weakDigest($except_session);
+ $except_session = PhabricatorAuthSession::newSessionDigest(
+ $except_session);
}
foreach ($sessions as $key => $session) {
if ($except_session !== null) {
$is_except = phutil_hashes_are_identical(
$session->getSessionKey(),
$except_session);
if ($is_except) {
continue;
}
}
$session->delete();
}
}
public function logoutSession(
PhabricatorUser $user,
PhabricatorAuthSession $session) {
$log = PhabricatorUserLog::initializeNewLog(
$user,
$user->getPHID(),
PhabricatorUserLog::ACTION_LOGOUT);
$log->save();
$extensions = PhabricatorAuthSessionEngineExtension::getAllExtensions();
foreach ($extensions as $extension) {
$extension->didLogout($user, array($session));
}
$session->delete();
}
/* -( High Security )------------------------------------------------------ */
/**
* Require the user respond to a high security (MFA) check.
*
* This method differs from @{method:requireHighSecuritySession} in that it
* does not upgrade the user's session as a side effect. This method is
* appropriate for one-time checks.
*
* @param PhabricatorUser User whose session needs to be in high security.
* @param AphrontReqeust Current request.
* @param string URI to return the user to if they cancel.
* @return PhabricatorAuthHighSecurityToken Security token.
* @task hisec
*/
public function requireHighSecurityToken(
PhabricatorUser $viewer,
AphrontRequest $request,
$cancel_uri) {
return $this->newHighSecurityToken(
$viewer,
$request,
$cancel_uri,
false,
false);
}
/**
* Require high security, or prompt the user to enter high security.
*
* If the user's session is in high security, this method will return a
* token. Otherwise, it will throw an exception which will eventually
* be converted into a multi-factor authentication workflow.
*
* This method upgrades the user's session to high security for a short
* period of time, and is appropriate if you anticipate they may need to
* take multiple high security actions. To perform a one-time check instead,
* use @{method:requireHighSecurityToken}.
*
* @param PhabricatorUser User whose session needs to be in high security.
* @param AphrontReqeust Current request.
* @param string URI to return the user to if they cancel.
* @param bool True to jump partial sessions directly into high
* security instead of just upgrading them to full
* sessions.
* @return PhabricatorAuthHighSecurityToken Security token.
* @task hisec
*/
public function requireHighSecuritySession(
PhabricatorUser $viewer,
AphrontRequest $request,
$cancel_uri,
$jump_into_hisec = false) {
return $this->newHighSecurityToken(
$viewer,
$request,
$cancel_uri,
false,
true);
}
private function newHighSecurityToken(
PhabricatorUser $viewer,
AphrontRequest $request,
$cancel_uri,
$jump_into_hisec,
$upgrade_session) {
if (!$viewer->hasSession()) {
throw new Exception(
pht('Requiring a high-security session from a user with no session!'));
}
// TODO: If a user answers a "requireHighSecurityToken()" prompt and hits
// a "requireHighSecuritySession()" prompt a short time later, the one-shot
// token should be good enough to upgrade the session.
$session = $viewer->getSession();
// Check if the session is already in high security mode.
$token = $this->issueHighSecurityToken($session);
if ($token) {
return $token;
}
// Load the multi-factor auth sources attached to this account.
$factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
'userPHID = %s',
$viewer->getPHID());
// If the account has no associated multi-factor auth, just issue a token
// without putting the session into high security mode. This is generally
// easier for users. A minor but desirable side effect is that when a user
// adds an auth factor, existing sessions won't get a free pass into hisec,
// since they never actually got marked as hisec.
if (!$factors) {
return $this->issueHighSecurityToken($session, true);
}
// Check for a rate limit without awarding points, so the user doesn't
// get partway through the workflow only to get blocked.
PhabricatorSystemActionEngine::willTakeAction(
array($viewer->getPHID()),
new PhabricatorAuthTryFactorAction(),
0);
$validation_results = array();
if ($request->isHTTPPost()) {
$request->validateCSRF();
if ($request->getExists(AphrontRequest::TYPE_HISEC)) {
// Limit factor verification rates to prevent brute force attacks.
PhabricatorSystemActionEngine::willTakeAction(
array($viewer->getPHID()),
new PhabricatorAuthTryFactorAction(),
1);
$ok = true;
foreach ($factors as $factor) {
$id = $factor->getID();
$impl = $factor->requireImplementation();
$validation_results[$id] = $impl->processValidateFactorForm(
$factor,
$viewer,
$request);
if (!$impl->isFactorValid($factor, $validation_results[$id])) {
$ok = false;
}
}
if ($ok) {
// Give the user a credit back for a successful factor verification.
PhabricatorSystemActionEngine::willTakeAction(
array($viewer->getPHID()),
new PhabricatorAuthTryFactorAction(),
-1);
if ($session->getIsPartial() && !$jump_into_hisec) {
// If we have a partial session and are not jumping directly into
// hisec, just issue a token without putting it in high security
// mode.
return $this->issueHighSecurityToken($session, true);
}
// If we aren't upgrading the session itself, just issue a token.
if (!$upgrade_session) {
return $this->issueHighSecurityToken($session, true);
}
$until = time() + phutil_units('15 minutes in seconds');
$session->setHighSecurityUntil($until);
queryfx(
$session->establishConnection('w'),
'UPDATE %T SET highSecurityUntil = %d WHERE id = %d',
$session->getTableName(),
$until,
$session->getID());
$log = PhabricatorUserLog::initializeNewLog(
$viewer,
$viewer->getPHID(),
PhabricatorUserLog::ACTION_ENTER_HISEC);
$log->save();
} else {
$log = PhabricatorUserLog::initializeNewLog(
$viewer,
$viewer->getPHID(),
PhabricatorUserLog::ACTION_FAIL_HISEC);
$log->save();
}
}
}
$token = $this->issueHighSecurityToken($session);
if ($token) {
return $token;
}
throw id(new PhabricatorAuthHighSecurityRequiredException())
->setCancelURI($cancel_uri)
->setFactors($factors)
->setFactorValidationResults($validation_results);
}
/**
* Issue a high security token for a session, if authorized.
*
* @param PhabricatorAuthSession Session to issue a token for.
* @param bool Force token issue.
* @return PhabricatorAuthHighSecurityToken|null Token, if authorized.
* @task hisec
*/
private function issueHighSecurityToken(
PhabricatorAuthSession $session,
$force = false) {
if ($session->isHighSecuritySession() || $force) {
return new PhabricatorAuthHighSecurityToken();
}
return null;
}
/**
* Render a form for providing relevant multi-factor credentials.
*
* @param PhabricatorUser Viewing user.
* @param AphrontRequest Current request.
* @return AphrontFormView Renderable form.
* @task hisec
*/
public function renderHighSecurityForm(
array $factors,
array $validation_results,
PhabricatorUser $viewer,
AphrontRequest $request) {
$form = id(new AphrontFormView())
->setUser($viewer)
->appendRemarkupInstructions('');
foreach ($factors as $factor) {
$factor->requireImplementation()->renderValidateFactorForm(
$factor,
$form,
$viewer,
idx($validation_results, $factor->getID()));
}
$form->appendRemarkupInstructions('');
return $form;
}
/**
* Strip the high security flag from a session.
*
* Kicks a session out of high security and logs the exit.
*
* @param PhabricatorUser Acting user.
* @param PhabricatorAuthSession Session to return to normal security.
* @return void
* @task hisec
*/
public function exitHighSecurity(
PhabricatorUser $viewer,
PhabricatorAuthSession $session) {
if (!$session->getHighSecurityUntil()) {
return;
}
queryfx(
$session->establishConnection('w'),
'UPDATE %T SET highSecurityUntil = NULL WHERE id = %d',
$session->getTableName(),
$session->getID());
$log = PhabricatorUserLog::initializeNewLog(
$viewer,
$viewer->getPHID(),
PhabricatorUserLog::ACTION_EXIT_HISEC);
$log->save();
}
/* -( Partial Sessions )--------------------------------------------------- */
/**
* Upgrade a partial session to a full session.
*
* @param PhabricatorAuthSession Session to upgrade.
* @return void
* @task partial
*/
public function upgradePartialSession(PhabricatorUser $viewer) {
if (!$viewer->hasSession()) {
throw new Exception(
pht('Upgrading partial session of user with no session!'));
}
$session = $viewer->getSession();
if (!$session->getIsPartial()) {
throw new Exception(pht('Session is not partial!'));
}
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$session->setIsPartial(0);
queryfx(
$session->establishConnection('w'),
'UPDATE %T SET isPartial = %d WHERE id = %d',
$session->getTableName(),
0,
$session->getID());
$log = PhabricatorUserLog::initializeNewLog(
$viewer,
$viewer->getPHID(),
PhabricatorUserLog::ACTION_LOGIN_FULL);
$log->save();
unset($unguarded);
}
/* -( Legalpad Documents )-------------------------------------------------- */
/**
* Upgrade a session to have all legalpad documents signed.
*
* @param PhabricatorUser User whose session should upgrade.
* @param array LegalpadDocument objects
* @return void
* @task partial
*/
public function signLegalpadDocuments(PhabricatorUser $viewer, array $docs) {
if (!$viewer->hasSession()) {
throw new Exception(
pht('Signing session legalpad documents of user with no session!'));
}
$session = $viewer->getSession();
if ($session->getSignedLegalpadDocuments()) {
throw new Exception(pht(
'Session has already signed required legalpad documents!'));
}
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$session->setSignedLegalpadDocuments(1);
queryfx(
$session->establishConnection('w'),
'UPDATE %T SET signedLegalpadDocuments = %d WHERE id = %d',
$session->getTableName(),
1,
$session->getID());
if (!empty($docs)) {
$log = PhabricatorUserLog::initializeNewLog(
$viewer,
$viewer->getPHID(),
PhabricatorUserLog::ACTION_LOGIN_LEGALPAD);
$log->save();
}
unset($unguarded);
}
/* -( One Time Login URIs )------------------------------------------------ */
/**
* Retrieve a temporary, one-time URI which can log in to an account.
*
* These URIs are used for password recovery and to regain access to accounts
* which users have been locked out of.
*
* @param PhabricatorUser User to generate a URI for.
* @param PhabricatorUserEmail Optionally, email to verify when
* link is used.
* @param string Optional context string for the URI. This is purely cosmetic
* and used only to customize workflow and error messages.
* @return string Login URI.
* @task onetime
*/
public function getOneTimeLoginURI(
PhabricatorUser $user,
PhabricatorUserEmail $email = null,
$type = self::ONETIME_RESET) {
$key = Filesystem::readRandomCharacters(32);
$key_hash = $this->getOneTimeLoginKeyHash($user, $email, $key);
$onetime_type = PhabricatorAuthOneTimeLoginTemporaryTokenType::TOKENTYPE;
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
id(new PhabricatorAuthTemporaryToken())
->setTokenResource($user->getPHID())
->setTokenType($onetime_type)
->setTokenExpires(time() + phutil_units('1 day in seconds'))
->setTokenCode($key_hash)
->save();
unset($unguarded);
$uri = '/login/once/'.$type.'/'.$user->getID().'/'.$key.'/';
if ($email) {
$uri = $uri.$email->getID().'/';
}
try {
$uri = PhabricatorEnv::getProductionURI($uri);
} catch (Exception $ex) {
// If a user runs `bin/auth recover` before configuring the base URI,
// just show the path. We don't have any way to figure out the domain.
// See T4132.
}
return $uri;
}
/**
* Load the temporary token associated with a given one-time login key.
*
* @param PhabricatorUser User to load the token for.
* @param PhabricatorUserEmail Optionally, email to verify when
* link is used.
* @param string Key user is presenting as a valid one-time login key.
* @return PhabricatorAuthTemporaryToken|null Token, if one exists.
* @task onetime
*/
public function loadOneTimeLoginKey(
PhabricatorUser $user,
PhabricatorUserEmail $email = null,
$key = null) {
$key_hash = $this->getOneTimeLoginKeyHash($user, $email, $key);
$onetime_type = PhabricatorAuthOneTimeLoginTemporaryTokenType::TOKENTYPE;
return id(new PhabricatorAuthTemporaryTokenQuery())
->setViewer($user)
->withTokenResources(array($user->getPHID()))
->withTokenTypes(array($onetime_type))
->withTokenCodes(array($key_hash))
->withExpired(false)
->executeOne();
}
/**
* Hash a one-time login key for storage as a temporary token.
*
* @param PhabricatorUser User this key is for.
* @param PhabricatorUserEmail Optionally, email to verify when
* link is used.
* @param string The one time login key.
* @return string Hash of the key.
* task onetime
*/
private function getOneTimeLoginKeyHash(
PhabricatorUser $user,
PhabricatorUserEmail $email = null,
$key = null) {
$parts = array(
$key,
$user->getAccountSecret(),
);
if ($email) {
$parts[] = $email->getVerificationCode();
}
return PhabricatorHash::weakDigest(implode(':', $parts));
}
/* -( User Cache )--------------------------------------------------------- */
/**
* @task cache
*/
private function getUserCacheQueryParts(AphrontDatabaseConnection $conn) {
$cache_selects = array();
$cache_joins = array();
$cache_map = array();
$keys = array();
$types_map = array();
$cache_types = PhabricatorUserCacheType::getAllCacheTypes();
foreach ($cache_types as $cache_type) {
foreach ($cache_type->getAutoloadKeys() as $autoload_key) {
$keys[] = $autoload_key;
$types_map[$autoload_key] = $cache_type;
}
}
$cache_table = id(new PhabricatorUserCache())->getTableName();
$cache_idx = 1;
foreach ($keys as $key) {
$join_as = 'ucache_'.$cache_idx;
$select_as = 'ucache_'.$cache_idx.'_v';
$cache_selects[] = qsprintf(
$conn,
'%T.cacheData %T',
$join_as,
$select_as);
$cache_joins[] = qsprintf(
$conn,
'LEFT JOIN %T AS %T ON u.phid = %T.userPHID
AND %T.cacheIndex = %s',
$cache_table,
$join_as,
$join_as,
$join_as,
PhabricatorHash::digestForIndex($key));
$cache_map[$select_as] = $key;
$cache_idx++;
}
if ($cache_selects) {
$cache_selects = qsprintf($conn, ', %LQ', $cache_selects);
} else {
$cache_selects = qsprintf($conn, '');
}
if ($cache_joins) {
$cache_joins = qsprintf($conn, '%LJ', $cache_joins);
} else {
$cache_joins = qsprintf($conn, '');
}
return array($cache_selects, $cache_joins, $cache_map, $types_map);
}
private function filterRawCacheData(
PhabricatorUser $user,
array $types_map,
array $cache_raw) {
foreach ($cache_raw as $cache_key => $cache_data) {
$type = $types_map[$cache_key];
if ($type->shouldValidateRawCacheData()) {
if (!$type->isRawCacheDataValid($user, $cache_key, $cache_data)) {
unset($cache_raw[$cache_key]);
}
}
}
return $cache_raw;
}
public function willServeRequestForUser(PhabricatorUser $user) {
// We allow the login user to generate any missing cache data inline.
$user->setAllowInlineCacheGeneration(true);
// Switch to the user's translation.
PhabricatorEnv::setLocaleCode($user->getTranslation());
$extensions = PhabricatorAuthSessionEngineExtension::getAllExtensions();
foreach ($extensions as $extension) {
$extension->willServeRequestForUser($user);
}
}
}
diff --git a/src/applications/auth/query/PhabricatorAuthSessionQuery.php b/src/applications/auth/query/PhabricatorAuthSessionQuery.php
index a2101201e2..00a663e964 100644
--- a/src/applications/auth/query/PhabricatorAuthSessionQuery.php
+++ b/src/applications/auth/query/PhabricatorAuthSessionQuery.php
@@ -1,116 +1,117 @@
<?php
final class PhabricatorAuthSessionQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $identityPHIDs;
private $sessionKeys;
private $sessionTypes;
public function withIdentityPHIDs(array $identity_phids) {
$this->identityPHIDs = $identity_phids;
return $this;
}
public function withSessionKeys(array $keys) {
$this->sessionKeys = $keys;
return $this;
}
public function withSessionTypes(array $types) {
$this->sessionTypes = $types;
return $this;
}
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function newResultObject() {
return new PhabricatorAuthSession();
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function willFilterPage(array $sessions) {
$identity_phids = mpull($sessions, 'getUserPHID');
$identity_objects = id(new PhabricatorObjectQuery())
->setViewer($this->getViewer())
->setParentQuery($this)
->withPHIDs($identity_phids)
->execute();
$identity_objects = mpull($identity_objects, null, 'getPHID');
foreach ($sessions as $key => $session) {
$identity_object = idx($identity_objects, $session->getUserPHID());
if (!$identity_object) {
unset($sessions[$key]);
} else {
$session->attachIdentityObject($identity_object);
}
}
return $sessions;
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'phid IN (%Ls)',
$this->phids);
}
if ($this->identityPHIDs !== null) {
$where[] = qsprintf(
$conn,
'userPHID IN (%Ls)',
$this->identityPHIDs);
}
if ($this->sessionKeys !== null) {
$hashes = array();
foreach ($this->sessionKeys as $session_key) {
- $hashes[] = PhabricatorHash::weakDigest($session_key);
+ $hashes[] = PhabricatorAuthSession::newSessionDigest(
+ new PhutilOpaqueEnvelope($session_key));
}
$where[] = qsprintf(
$conn,
'sessionKey IN (%Ls)',
$hashes);
}
if ($this->sessionTypes !== null) {
$where[] = qsprintf(
$conn,
'type IN (%Ls)',
$this->sessionTypes);
}
return $where;
}
public function getQueryApplicationClass() {
return 'PhabricatorAuthApplication';
}
}
diff --git a/src/applications/auth/storage/PhabricatorAuthSession.php b/src/applications/auth/storage/PhabricatorAuthSession.php
index c4731b669b..6d54dda781 100644
--- a/src/applications/auth/storage/PhabricatorAuthSession.php
+++ b/src/applications/auth/storage/PhabricatorAuthSession.php
@@ -1,130 +1,138 @@
<?php
final class PhabricatorAuthSession extends PhabricatorAuthDAO
implements PhabricatorPolicyInterface {
const TYPE_WEB = 'web';
const TYPE_CONDUIT = 'conduit';
+ const SESSION_DIGEST_KEY = 'session.digest';
+
protected $userPHID;
protected $type;
protected $sessionKey;
protected $sessionStart;
protected $sessionExpires;
protected $highSecurityUntil;
protected $isPartial;
protected $signedLegalpadDocuments;
private $identityObject = self::ATTACHABLE;
+ public static function newSessionDigest(PhutilOpaqueEnvelope $session_token) {
+ return PhabricatorHash::digestWithNamedKey(
+ $session_token->openEnvelope(),
+ self::SESSION_DIGEST_KEY);
+ }
+
protected function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'type' => 'text32',
- 'sessionKey' => 'bytes40',
+ 'sessionKey' => 'text64',
'sessionStart' => 'epoch',
'sessionExpires' => 'epoch',
'highSecurityUntil' => 'epoch?',
'isPartial' => 'bool',
'signedLegalpadDocuments' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'sessionKey' => array(
'columns' => array('sessionKey'),
'unique' => true,
),
'key_identity' => array(
'columns' => array('userPHID', 'type'),
),
'key_expires' => array(
'columns' => array('sessionExpires'),
),
),
) + parent::getConfiguration();
}
public function getApplicationName() {
// This table predates the "Auth" application, and really all applications.
return 'user';
}
public function getTableName() {
// This is a very old table with a nonstandard name.
return PhabricatorUser::SESSION_TABLE;
}
public function attachIdentityObject($identity_object) {
$this->identityObject = $identity_object;
return $this;
}
public function getIdentityObject() {
return $this->assertAttached($this->identityObject);
}
public static function getSessionTypeTTL($session_type) {
switch ($session_type) {
case self::TYPE_WEB:
return phutil_units('30 days in seconds');
case self::TYPE_CONDUIT:
return phutil_units('24 hours in seconds');
default:
throw new Exception(pht('Unknown session type "%s".', $session_type));
}
}
public function getPHIDType() {
return PhabricatorAuthSessionPHIDType::TYPECONST;
}
public function isHighSecuritySession() {
$until = $this->getHighSecurityUntil();
if (!$until) {
return false;
}
$now = PhabricatorTime::getNow();
if ($until < $now) {
return false;
}
return true;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
return PhabricatorPolicies::POLICY_NOONE;
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
if (!$viewer->getPHID()) {
return false;
}
$object = $this->getIdentityObject();
if ($object instanceof PhabricatorUser) {
return ($object->getPHID() == $viewer->getPHID());
} else if ($object instanceof PhabricatorExternalAccount) {
return ($object->getUserPHID() == $viewer->getPHID());
}
return false;
}
public function describeAutomaticCapability($capability) {
return pht('A session is visible only to its owner.');
}
}
diff --git a/src/applications/people/storage/PhabricatorUserLog.php b/src/applications/people/storage/PhabricatorUserLog.php
index 82819f8f5b..12cb4cb626 100644
--- a/src/applications/people/storage/PhabricatorUserLog.php
+++ b/src/applications/people/storage/PhabricatorUserLog.php
@@ -1,223 +1,223 @@
<?php
final class PhabricatorUserLog extends PhabricatorUserDAO
implements PhabricatorPolicyInterface {
const ACTION_LOGIN = 'login';
const ACTION_LOGIN_PARTIAL = 'login-partial';
const ACTION_LOGIN_FULL = 'login-full';
const ACTION_LOGOUT = 'logout';
const ACTION_LOGIN_FAILURE = 'login-fail';
const ACTION_LOGIN_LEGALPAD = 'login-legalpad';
const ACTION_RESET_PASSWORD = 'reset-pass';
const ACTION_CREATE = 'create';
const ACTION_EDIT = 'edit';
const ACTION_ADMIN = 'admin';
const ACTION_SYSTEM_AGENT = 'system-agent';
const ACTION_MAILING_LIST = 'mailing-list';
const ACTION_DISABLE = 'disable';
const ACTION_APPROVE = 'approve';
const ACTION_DELETE = 'delete';
const ACTION_CONDUIT_CERTIFICATE = 'conduit-cert';
const ACTION_CONDUIT_CERTIFICATE_FAILURE = 'conduit-cert-fail';
const ACTION_EMAIL_PRIMARY = 'email-primary';
const ACTION_EMAIL_REMOVE = 'email-remove';
const ACTION_EMAIL_ADD = 'email-add';
const ACTION_EMAIL_VERIFY = 'email-verify';
const ACTION_EMAIL_REASSIGN = 'email-reassign';
const ACTION_CHANGE_PASSWORD = 'change-password';
const ACTION_CHANGE_USERNAME = 'change-username';
const ACTION_ENTER_HISEC = 'hisec-enter';
const ACTION_EXIT_HISEC = 'hisec-exit';
const ACTION_FAIL_HISEC = 'hisec-fail';
const ACTION_MULTI_ADD = 'multi-add';
const ACTION_MULTI_REMOVE = 'multi-remove';
protected $actorPHID;
protected $userPHID;
protected $action;
protected $oldValue;
protected $newValue;
protected $details = array();
protected $remoteAddr;
protected $session;
public static function getActionTypeMap() {
return array(
self::ACTION_LOGIN => pht('Login'),
self::ACTION_LOGIN_PARTIAL => pht('Login: Partial Login'),
self::ACTION_LOGIN_FULL => pht('Login: Upgrade to Full'),
self::ACTION_LOGIN_FAILURE => pht('Login: Failure'),
self::ACTION_LOGIN_LEGALPAD =>
pht('Login: Signed Required Legalpad Documents'),
self::ACTION_LOGOUT => pht('Logout'),
self::ACTION_RESET_PASSWORD => pht('Reset Password'),
self::ACTION_CREATE => pht('Create Account'),
self::ACTION_EDIT => pht('Edit Account'),
self::ACTION_ADMIN => pht('Add/Remove Administrator'),
self::ACTION_SYSTEM_AGENT => pht('Add/Remove System Agent'),
self::ACTION_MAILING_LIST => pht('Add/Remove Mailing List'),
self::ACTION_DISABLE => pht('Enable/Disable'),
self::ACTION_APPROVE => pht('Approve Registration'),
self::ACTION_DELETE => pht('Delete User'),
self::ACTION_CONDUIT_CERTIFICATE
=> pht('Conduit: Read Certificate'),
self::ACTION_CONDUIT_CERTIFICATE_FAILURE
=> pht('Conduit: Read Certificate Failure'),
self::ACTION_EMAIL_PRIMARY => pht('Email: Change Primary'),
self::ACTION_EMAIL_ADD => pht('Email: Add Address'),
self::ACTION_EMAIL_REMOVE => pht('Email: Remove Address'),
self::ACTION_EMAIL_VERIFY => pht('Email: Verify'),
self::ACTION_EMAIL_REASSIGN => pht('Email: Reassign'),
self::ACTION_CHANGE_PASSWORD => pht('Change Password'),
self::ACTION_CHANGE_USERNAME => pht('Change Username'),
self::ACTION_ENTER_HISEC => pht('Hisec: Enter'),
self::ACTION_EXIT_HISEC => pht('Hisec: Exit'),
self::ACTION_FAIL_HISEC => pht('Hisec: Failed Attempt'),
self::ACTION_MULTI_ADD => pht('Multi-Factor: Add Factor'),
self::ACTION_MULTI_REMOVE => pht('Multi-Factor: Remove Factor'),
);
}
public static function initializeNewLog(
PhabricatorUser $actor = null,
$object_phid = null,
$action = null) {
$log = new PhabricatorUserLog();
if ($actor) {
$log->setActorPHID($actor->getPHID());
if ($actor->hasSession()) {
$session = $actor->getSession();
// NOTE: This is a hash of the real session value, so it's safe to
// store it directly in the logs.
$log->setSession($session->getSessionKey());
}
}
$log->setUserPHID((string)$object_phid);
$log->setAction($action);
$address = PhabricatorEnv::getRemoteAddress();
if ($address) {
$log->remoteAddr = $address->getAddress();
} else {
$log->remoteAddr = '';
}
return $log;
}
public static function loadRecentEventsFromThisIP($action, $timespan) {
$address = PhabricatorEnv::getRemoteAddress();
if (!$address) {
return array();
}
return id(new PhabricatorUserLog())->loadAllWhere(
'action = %s AND remoteAddr = %s AND dateCreated > %d
ORDER BY dateCreated DESC',
$action,
$address->getAddress(),
PhabricatorTime::getNow() - $timespan);
}
public function save() {
$this->details['host'] = php_uname('n');
$this->details['user_agent'] = AphrontRequest::getHTTPHeader('User-Agent');
return parent::save();
}
protected function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'oldValue' => self::SERIALIZATION_JSON,
'newValue' => self::SERIALIZATION_JSON,
'details' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'actorPHID' => 'phid?',
'action' => 'text64',
'remoteAddr' => 'text64',
- 'session' => 'bytes40?',
+ 'session' => 'text64?',
),
self::CONFIG_KEY_SCHEMA => array(
'actorPHID' => array(
'columns' => array('actorPHID', 'dateCreated'),
),
'userPHID' => array(
'columns' => array('userPHID', 'dateCreated'),
),
'action' => array(
'columns' => array('action', 'dateCreated'),
),
'dateCreated' => array(
'columns' => array('dateCreated'),
),
'remoteAddr' => array(
'columns' => array('remoteAddr', 'dateCreated'),
),
'session' => array(
'columns' => array('session', 'dateCreated'),
),
),
) + parent::getConfiguration();
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return PhabricatorPolicies::POLICY_NOONE;
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
if ($viewer->getIsAdmin()) {
return true;
}
$viewer_phid = $viewer->getPHID();
if ($viewer_phid) {
$user_phid = $this->getUserPHID();
if ($viewer_phid == $user_phid) {
return true;
}
$actor_phid = $this->getActorPHID();
if ($viewer_phid == $actor_phid) {
return true;
}
}
return false;
}
public function describeAutomaticCapability($capability) {
return array(
pht('Users can view their activity and activity that affects them.'),
pht('Administrators can always view all activity.'),
);
}
}
diff --git a/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php b/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php
index ae653e0f70..5fada0bbed 100644
--- a/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php
@@ -1,319 +1,320 @@
<?php
final class PhabricatorMultiFactorSettingsPanel
extends PhabricatorSettingsPanel {
public function getPanelKey() {
return 'multifactor';
}
public function getPanelName() {
return pht('Multi-Factor Auth');
}
public function getPanelGroupKey() {
return PhabricatorSettingsAuthenticationPanelGroup::PANELGROUPKEY;
}
public function processRequest(AphrontRequest $request) {
if ($request->getExists('new')) {
return $this->processNew($request);
}
if ($request->getExists('edit')) {
return $this->processEdit($request);
}
if ($request->getExists('delete')) {
return $this->processDelete($request);
}
$user = $this->getUser();
$viewer = $request->getUser();
$factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
'userPHID = %s',
$user->getPHID());
$rows = array();
$rowc = array();
$highlight_id = $request->getInt('id');
foreach ($factors as $factor) {
$impl = $factor->getImplementation();
if ($impl) {
$type = $impl->getFactorName();
} else {
$type = $factor->getFactorKey();
}
if ($factor->getID() == $highlight_id) {
$rowc[] = 'highlighted';
} else {
$rowc[] = null;
}
$rows[] = array(
javelin_tag(
'a',
array(
'href' => $this->getPanelURI('?edit='.$factor->getID()),
'sigil' => 'workflow',
),
$factor->getFactorName()),
$type,
phabricator_datetime($factor->getDateCreated(), $viewer),
javelin_tag(
'a',
array(
'href' => $this->getPanelURI('?delete='.$factor->getID()),
'sigil' => 'workflow',
'class' => 'small button button-grey',
),
pht('Remove')),
);
}
$table = new AphrontTableView($rows);
$table->setNoDataString(
pht("You haven't added any authentication factors to your account yet."));
$table->setHeaders(
array(
pht('Name'),
pht('Type'),
pht('Created'),
'',
));
$table->setColumnClasses(
array(
'wide pri',
'',
'right',
'action',
));
$table->setRowClasses($rowc);
$table->setDeviceVisibility(
array(
true,
false,
false,
true,
));
$help_uri = PhabricatorEnv::getDoclink(
'User Guide: Multi-Factor Authentication');
$buttons = array();
$buttons[] = id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-plus')
->setText(pht('Add Auth Factor'))
->setHref($this->getPanelURI('?new=true'))
->setWorkflow(true)
->setColor(PHUIButtonView::GREY);
$buttons[] = id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-book')
->setText(pht('Help'))
->setHref($help_uri)
->setColor(PHUIButtonView::GREY);
return $this->newBox(pht('Authentication Factors'), $table, $buttons);
}
private function processNew(AphrontRequest $request) {
$viewer = $request->getUser();
$user = $this->getUser();
$token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
$viewer,
$request,
$this->getPanelURI());
$factors = PhabricatorAuthFactor::getAllFactors();
$form = id(new AphrontFormView())
->setUser($viewer);
$type = $request->getStr('type');
if (empty($factors[$type]) || !$request->isFormPost()) {
$factor = null;
} else {
$factor = $factors[$type];
}
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->addHiddenInput('new', true);
if ($factor === null) {
$choice_control = id(new AphrontFormRadioButtonControl())
->setName('type')
->setValue(key($factors));
foreach ($factors as $available_factor) {
$choice_control->addButton(
$available_factor->getFactorKey(),
$available_factor->getFactorName(),
$available_factor->getFactorDescription());
}
$dialog->appendParagraph(
pht(
'Adding an additional authentication factor improves the security '.
'of your account. Choose the type of factor to add:'));
$form
->appendChild($choice_control);
} else {
$dialog->addHiddenInput('type', $type);
$config = $factor->processAddFactorForm(
$form,
$request,
$user);
if ($config) {
$config->save();
$log = PhabricatorUserLog::initializeNewLog(
$viewer,
$user->getPHID(),
PhabricatorUserLog::ACTION_MULTI_ADD);
$log->save();
$user->updateMultiFactorEnrollment();
// Terminate other sessions so they must log in and survive the
// multi-factor auth check.
id(new PhabricatorAuthSessionEngine())->terminateLoginSessions(
$user,
- $request->getCookie(PhabricatorCookies::COOKIE_SESSION));
+ new PhutilOpaqueEnvelope(
+ $request->getCookie(PhabricatorCookies::COOKIE_SESSION)));
return id(new AphrontRedirectResponse())
->setURI($this->getPanelURI('?id='.$config->getID()));
}
}
$dialog
->setWidth(AphrontDialogView::WIDTH_FORM)
->setTitle(pht('Add Authentication Factor'))
->appendChild($form->buildLayoutView())
->addSubmitButton(pht('Continue'))
->addCancelButton($this->getPanelURI());
return id(new AphrontDialogResponse())
->setDialog($dialog);
}
private function processEdit(AphrontRequest $request) {
$viewer = $request->getUser();
$user = $this->getUser();
$factor = id(new PhabricatorAuthFactorConfig())->loadOneWhere(
'id = %d AND userPHID = %s',
$request->getInt('edit'),
$user->getPHID());
if (!$factor) {
return new Aphront404Response();
}
$e_name = true;
$errors = array();
if ($request->isFormPost()) {
$name = $request->getStr('name');
if (!strlen($name)) {
$e_name = pht('Required');
$errors[] = pht(
'Authentication factors must have a name to identify them.');
}
if (!$errors) {
$factor->setFactorName($name);
$factor->save();
$user->updateMultiFactorEnrollment();
return id(new AphrontRedirectResponse())
->setURI($this->getPanelURI('?id='.$factor->getID()));
}
} else {
$name = $factor->getFactorName();
}
$form = id(new AphrontFormView())
->setUser($viewer)
->appendChild(
id(new AphrontFormTextControl())
->setName('name')
->setLabel(pht('Name'))
->setValue($name)
->setError($e_name));
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->addHiddenInput('edit', $factor->getID())
->setTitle(pht('Edit Authentication Factor'))
->setErrors($errors)
->appendChild($form->buildLayoutView())
->addSubmitButton(pht('Save'))
->addCancelButton($this->getPanelURI());
return id(new AphrontDialogResponse())
->setDialog($dialog);
}
private function processDelete(AphrontRequest $request) {
$viewer = $request->getUser();
$user = $this->getUser();
$token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
$viewer,
$request,
$this->getPanelURI());
$factor = id(new PhabricatorAuthFactorConfig())->loadOneWhere(
'id = %d AND userPHID = %s',
$request->getInt('delete'),
$user->getPHID());
if (!$factor) {
return new Aphront404Response();
}
if ($request->isFormPost()) {
$factor->delete();
$log = PhabricatorUserLog::initializeNewLog(
$viewer,
$user->getPHID(),
PhabricatorUserLog::ACTION_MULTI_REMOVE);
$log->save();
$user->updateMultiFactorEnrollment();
return id(new AphrontRedirectResponse())
->setURI($this->getPanelURI());
}
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->addHiddenInput('delete', $factor->getID())
->setTitle(pht('Delete Authentication Factor'))
->appendParagraph(
pht(
'Really remove the authentication factor %s from your account?',
phutil_tag('strong', array(), $factor->getFactorName())))
->addSubmitButton(pht('Remove Factor'))
->addCancelButton($this->getPanelURI());
return id(new AphrontDialogResponse())
->setDialog($dialog);
}
}
diff --git a/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php b/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php
index 9fb8838cf7..79d7610f2f 100644
--- a/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php
@@ -1,217 +1,218 @@
<?php
final class PhabricatorPasswordSettingsPanel extends PhabricatorSettingsPanel {
public function getPanelKey() {
return 'password';
}
public function getPanelName() {
return pht('Password');
}
public function getPanelGroupKey() {
return PhabricatorSettingsAuthenticationPanelGroup::PANELGROUPKEY;
}
public function isEnabled() {
// There's no sense in showing a change password panel if this install
// doesn't support password authentication.
if (!PhabricatorPasswordAuthProvider::getPasswordProvider()) {
return false;
}
return true;
}
public function processRequest(AphrontRequest $request) {
$viewer = $request->getUser();
$user = $this->getUser();
$content_source = PhabricatorContentSource::newFromRequest($request);
$token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
$viewer,
$request,
'/settings/');
$min_len = PhabricatorEnv::getEnvConfig('account.minimum-password-length');
$min_len = (int)$min_len;
// NOTE: Users can also change passwords through the separate "set/reset"
// interface which is reached by logging in with a one-time token after
// registration or password reset. If this flow changes, that flow may
// also need to change.
$account_type = PhabricatorAuthPassword::PASSWORD_TYPE_ACCOUNT;
$password_objects = id(new PhabricatorAuthPasswordQuery())
->setViewer($viewer)
->withObjectPHIDs(array($user->getPHID()))
->withPasswordTypes(array($account_type))
->withIsRevoked(false)
->execute();
if ($password_objects) {
$password_object = head($password_objects);
} else {
$password_object = PhabricatorAuthPassword::initializeNewPassword(
$user,
$account_type);
}
$e_old = true;
$e_new = true;
$e_conf = true;
$errors = array();
if ($request->isFormPost()) {
// Rate limit guesses about the old password. This page requires MFA and
// session compromise already, so this is mostly just to stop researchers
// from reporting this as a vulnerability.
PhabricatorSystemActionEngine::willTakeAction(
array($viewer->getPHID()),
new PhabricatorAuthChangePasswordAction(),
1);
$envelope = new PhutilOpaqueEnvelope($request->getStr('old_pw'));
$engine = id(new PhabricatorAuthPasswordEngine())
->setViewer($viewer)
->setContentSource($content_source)
->setPasswordType($account_type)
->setObject($user);
if (!strlen($envelope->openEnvelope())) {
$errors[] = pht('You must enter your current password.');
$e_old = pht('Required');
} else if (!$engine->isValidPassword($envelope)) {
$errors[] = pht('The old password you entered is incorrect.');
$e_old = pht('Invalid');
} else {
$e_old = null;
// Refund the user an action credit for getting the password right.
PhabricatorSystemActionEngine::willTakeAction(
array($viewer->getPHID()),
new PhabricatorAuthChangePasswordAction(),
-1);
}
$pass = $request->getStr('new_pw');
$conf = $request->getStr('conf_pw');
$password_envelope = new PhutilOpaqueEnvelope($pass);
$confirm_envelope = new PhutilOpaqueEnvelope($conf);
try {
$engine->checkNewPassword($password_envelope, $confirm_envelope);
$e_new = null;
$e_conf = null;
} catch (PhabricatorAuthPasswordException $ex) {
$errors[] = $ex->getMessage();
$e_new = $ex->getPasswordError();
$e_conf = $ex->getConfirmError();
}
if (!$errors) {
$password_object
->setPassword($password_envelope, $user)
->save();
$next = $this->getPanelURI('?saved=true');
id(new PhabricatorAuthSessionEngine())->terminateLoginSessions(
$user,
- $request->getCookie(PhabricatorCookies::COOKIE_SESSION));
+ new PhutilOpaqueEnvelope(
+ $request->getCookie(PhabricatorCookies::COOKIE_SESSION)));
return id(new AphrontRedirectResponse())->setURI($next);
}
}
if ($password_object->getID()) {
try {
$can_upgrade = $password_object->canUpgrade();
} catch (PhabricatorPasswordHasherUnavailableException $ex) {
$can_upgrade = false;
$errors[] = pht(
'Your password is currently hashed using an algorithm which is '.
'no longer available on this install.');
$errors[] = pht(
'Because the algorithm implementation is missing, your password '.
'can not be used or updated.');
$errors[] = pht(
'To set a new password, request a password reset link from the '.
'login screen and then follow the instructions.');
}
if ($can_upgrade) {
$errors[] = pht(
'The strength of your stored password hash can be upgraded. '.
'To upgrade, either: log out and log in using your password; or '.
'change your password.');
}
}
$len_caption = null;
if ($min_len) {
$len_caption = pht('Minimum password length: %d characters.', $min_len);
}
$form = id(new AphrontFormView())
->setViewer($viewer)
->appendChild(
id(new AphrontFormPasswordControl())
->setLabel(pht('Old Password'))
->setError($e_old)
->setName('old_pw'))
->appendChild(
id(new AphrontFormPasswordControl())
->setDisableAutocomplete(true)
->setLabel(pht('New Password'))
->setError($e_new)
->setName('new_pw'))
->appendChild(
id(new AphrontFormPasswordControl())
->setDisableAutocomplete(true)
->setLabel(pht('Confirm Password'))
->setCaption($len_caption)
->setError($e_conf)
->setName('conf_pw'))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Change Password')));
$properties = id(new PHUIPropertyListView());
$properties->addProperty(
pht('Current Algorithm'),
PhabricatorPasswordHasher::getCurrentAlgorithmName(
$password_object->newPasswordEnvelope()));
$properties->addProperty(
pht('Best Available Algorithm'),
PhabricatorPasswordHasher::getBestAlgorithmName());
$info_view = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->appendChild(
pht('Changing your password will terminate any other outstanding '.
'login sessions.'));
$algo_box = $this->newBox(pht('Password Algorithms'), $properties);
$form_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Change Password'))
->setFormSaved($request->getStr('saved'))
->setFormErrors($errors)
->setBackground(PHUIObjectBoxView::WHITE_CONFIG)
->setForm($form);
return array(
$form_box,
$algo_box,
$info_view,
);
}
}
diff --git a/src/applications/settings/panel/PhabricatorSessionsSettingsPanel.php b/src/applications/settings/panel/PhabricatorSessionsSettingsPanel.php
index eab18002a1..314d68f69d 100644
--- a/src/applications/settings/panel/PhabricatorSessionsSettingsPanel.php
+++ b/src/applications/settings/panel/PhabricatorSessionsSettingsPanel.php
@@ -1,138 +1,139 @@
<?php
final class PhabricatorSessionsSettingsPanel extends PhabricatorSettingsPanel {
public function getPanelKey() {
return 'sessions';
}
public function getPanelName() {
return pht('Sessions');
}
public function getPanelGroupKey() {
return PhabricatorSettingsLogsPanelGroup::PANELGROUPKEY;
}
public function isEnabled() {
return true;
}
public function processRequest(AphrontRequest $request) {
$viewer = $request->getUser();
$accounts = id(new PhabricatorExternalAccountQuery())
->setViewer($viewer)
->withUserPHIDs(array($viewer->getPHID()))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->execute();
$identity_phids = mpull($accounts, 'getPHID');
$identity_phids[] = $viewer->getPHID();
$sessions = id(new PhabricatorAuthSessionQuery())
->setViewer($viewer)
->withIdentityPHIDs($identity_phids)
->execute();
$handles = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs($identity_phids)
->execute();
- $current_key = PhabricatorHash::weakDigest(
- $request->getCookie(PhabricatorCookies::COOKIE_SESSION));
+ $current_key = PhabricatorAuthSession::newSessionDigest(
+ new PhutilOpaqueEnvelope(
+ $request->getCookie(PhabricatorCookies::COOKIE_SESSION)));
$rows = array();
$rowc = array();
foreach ($sessions as $session) {
$is_current = phutil_hashes_are_identical(
$session->getSessionKey(),
$current_key);
if ($is_current) {
$rowc[] = 'highlighted';
$button = phutil_tag(
'a',
array(
'class' => 'small button button-grey disabled',
),
pht('Current'));
} else {
$rowc[] = null;
$button = javelin_tag(
'a',
array(
'href' => '/auth/session/terminate/'.$session->getID().'/',
'class' => 'small button button-grey',
'sigil' => 'workflow',
),
pht('Terminate'));
}
$hisec = ($session->getHighSecurityUntil() - time());
$rows[] = array(
$handles[$session->getUserPHID()]->renderLink(),
substr($session->getSessionKey(), 0, 6),
$session->getType(),
($hisec > 0)
? phutil_format_relative_time($hisec)
: null,
phabricator_datetime($session->getSessionStart(), $viewer),
phabricator_date($session->getSessionExpires(), $viewer),
$button,
);
}
$table = new AphrontTableView($rows);
$table->setNoDataString(pht("You don't have any active sessions."));
$table->setRowClasses($rowc);
$table->setHeaders(
array(
pht('Identity'),
pht('Session'),
pht('Type'),
pht('HiSec'),
pht('Created'),
pht('Expires'),
pht(''),
));
$table->setColumnClasses(
array(
'wide',
'n',
'',
'right',
'right',
'right',
'action',
));
$buttons = array();
$buttons[] = id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-warning')
->setText(pht('Terminate All Sessions'))
->setHref('/auth/session/terminate/all/')
->setWorkflow(true)
->setColor(PHUIButtonView::RED);
$hisec = ($viewer->getSession()->getHighSecurityUntil() - time());
if ($hisec > 0) {
$buttons[] = id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-lock')
->setText(pht('Leave High Security'))
->setHref('/auth/session/downgrade/')
->setWorkflow(true)
->setColor(PHUIButtonView::RED);
}
return $this->newBox(pht('Active Login Sessions'), $table, $buttons);
}
}
diff --git a/src/infrastructure/util/PhabricatorHash.php b/src/infrastructure/util/PhabricatorHash.php
index ce48b0966b..d717778eb3 100644
--- a/src/infrastructure/util/PhabricatorHash.php
+++ b/src/infrastructure/util/PhabricatorHash.php
@@ -1,269 +1,281 @@
<?php
final class PhabricatorHash extends Phobject {
const INDEX_DIGEST_LENGTH = 12;
const ANCHOR_DIGEST_LENGTH = 12;
/**
* Digest a string using HMAC+SHA1.
*
* Because a SHA1 collision is now known, this method should be considered
* weak. Callers should prefer @{method:digestWithNamedKey}.
*
* @param string Input string.
* @return string 32-byte hexadecimal SHA1+HMAC hash.
*/
public static function weakDigest($string, $key = null) {
if ($key === null) {
$key = PhabricatorEnv::getEnvConfig('security.hmac-key');
}
if (!$key) {
throw new Exception(
pht(
"Set a '%s' in your Phabricator configuration!",
'security.hmac-key'));
}
return hash_hmac('sha1', $string, $key);
}
/**
* Digest a string for use in, e.g., a MySQL index. This produces a short
* (12-byte), case-sensitive alphanumeric string with 72 bits of entropy,
* which is generally safe in most contexts (notably, URLs).
*
* This method emphasizes compactness, and should not be used for security
* related hashing (for general purpose hashing, see @{method:digest}).
*
* @param string Input string.
* @return string 12-byte, case-sensitive, mostly-alphanumeric hash of
* the string.
*/
public static function digestForIndex($string) {
$hash = sha1($string, $raw_output = true);
static $map;
if ($map === null) {
$map = '0123456789'.
'abcdefghij'.
'klmnopqrst'.
'uvwxyzABCD'.
'EFGHIJKLMN'.
'OPQRSTUVWX'.
'YZ._';
}
$result = '';
for ($ii = 0; $ii < self::INDEX_DIGEST_LENGTH; $ii++) {
$result .= $map[(ord($hash[$ii]) & 0x3F)];
}
return $result;
}
/**
* Digest a string for use in HTML page anchors. This is similar to
* @{method:digestForIndex} but produces purely alphanumeric output.
*
* This tries to be mostly compatible with the index digest to limit how
* much stuff we're breaking by switching to it. For additional discussion,
* see T13045.
*
* @param string Input string.
* @return string 12-byte, case-sensitive, purely-alphanumeric hash of
* the string.
*/
public static function digestForAnchor($string) {
$hash = sha1($string, $raw_output = true);
static $map;
if ($map === null) {
$map = '0123456789'.
'abcdefghij'.
'klmnopqrst'.
'uvwxyzABCD'.
'EFGHIJKLMN'.
'OPQRSTUVWX'.
'YZ';
}
$result = '';
$accum = 0;
$map_size = strlen($map);
for ($ii = 0; $ii < self::ANCHOR_DIGEST_LENGTH; $ii++) {
$byte = ord($hash[$ii]);
$low_bits = ($byte & 0x3F);
$accum = ($accum + $byte) % $map_size;
if ($low_bits < $map_size) {
// If an index digest would produce any alphanumeric character, just
// use that character. This means that these digests are the same as
// digests created with "digestForIndex()" in all positions where the
// output character is some character other than "." or "_".
$result .= $map[$low_bits];
} else {
// If an index digest would produce a non-alphumeric character ("." or
// "_"), pick an alphanumeric character instead. We accumulate an
// index into the alphanumeric character list to try to preserve
// entropy here. We could use this strategy for all bytes instead,
// but then these digests would differ from digests created with
// "digestForIndex()" in all positions, instead of just a small number
// of positions.
$result .= $map[$accum];
}
}
return $result;
}
public static function digestToRange($string, $min, $max) {
if ($min > $max) {
throw new Exception(pht('Maximum must be larger than minimum.'));
}
if ($min == $max) {
return $min;
}
$hash = sha1($string, $raw_output = true);
// Make sure this ends up positive, even on 32-bit machines.
$value = head(unpack('L', $hash)) & 0x7FFFFFFF;
return $min + ($value % (1 + $max - $min));
}
/**
* Shorten a string to a maximum byte length in a collision-resistant way
* while retaining some degree of human-readability.
*
* This function converts an input string into a prefix plus a hash. For
* example, a very long string beginning with "crabapplepie..." might be
* digested to something like "crabapp-N1wM1Nz3U84k".
*
* This allows the maximum length of identifiers to be fixed while
* maintaining a high degree of collision resistance and a moderate degree
* of human readability.
*
* @param string The string to shorten.
* @param int Maximum length of the result.
* @return string String shortened in a collision-resistant way.
*/
public static function digestToLength($string, $length) {
// We need at least two more characters than the hash length to fit in a
// a 1-character prefix and a separator.
$min_length = self::INDEX_DIGEST_LENGTH + 2;
if ($length < $min_length) {
throw new Exception(
pht(
'Length parameter in %s must be at least %s, '.
'but %s was provided.',
'digestToLength()',
new PhutilNumber($min_length),
new PhutilNumber($length)));
}
// We could conceivably return the string unmodified if it's shorter than
// the specified length. Instead, always hash it. This makes the output of
// the method more recognizable and consistent (no surprising new behavior
// once you hit a string longer than `$length`) and prevents an attacker
// who can control the inputs from intentionally using the hashed form
// of a string to cause a collision.
$hash = self::digestForIndex($string);
$prefix = substr($string, 0, ($length - ($min_length - 1)));
return $prefix.'-'.$hash;
}
public static function digestWithNamedKey($message, $key_name) {
$key_bytes = self::getNamedHMACKey($key_name);
return self::digestHMACSHA256($message, $key_bytes);
}
public static function digestHMACSHA256($message, $key) {
+ if (!is_string($message)) {
+ throw new Exception(
+ pht('HMAC-SHA256 can only digest strings.'));
+ }
+
+ if (!is_string($key)) {
+ throw new Exception(
+ pht('HMAC-SHA256 keys must be strings.'));
+ }
+
if (!strlen($key)) {
throw new Exception(
pht('HMAC-SHA256 requires a nonempty key.'));
}
$result = hash_hmac('sha256', $message, $key, $raw_output = false);
- if ($result === false) {
+ // Although "hash_hmac()" is documented as returning `false` when it fails,
+ // it can also return `null` if you pass an object as the "$message".
+ if ($result === false || $result === null) {
throw new Exception(
pht('Unable to compute HMAC-SHA256 digest of message.'));
}
return $result;
}
/* -( HMAC Key Management )------------------------------------------------ */
private static function getNamedHMACKey($hmac_name) {
$cache = PhabricatorCaches::getImmutableCache();
$cache_key = "hmac.key({$hmac_name})";
$hmac_key = $cache->getKey($cache_key);
if (!strlen($hmac_key)) {
$hmac_key = self::readHMACKey($hmac_name);
if ($hmac_key === null) {
$hmac_key = self::newHMACKey($hmac_name);
self::writeHMACKey($hmac_name, $hmac_key);
}
$cache->setKey($cache_key, $hmac_key);
}
// The "hex2bin()" function doesn't exist until PHP 5.4.0 so just
// implement it inline.
$result = '';
for ($ii = 0; $ii < strlen($hmac_key); $ii += 2) {
$result .= pack('H*', substr($hmac_key, $ii, 2));
}
return $result;
}
private static function newHMACKey($hmac_name) {
$hmac_key = Filesystem::readRandomBytes(64);
return bin2hex($hmac_key);
}
private static function writeHMACKey($hmac_name, $hmac_key) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
id(new PhabricatorAuthHMACKey())
->setKeyName($hmac_name)
->setKeyValue($hmac_key)
->save();
unset($unguarded);
}
private static function readHMACKey($hmac_name) {
$table = new PhabricatorAuthHMACKey();
$conn = $table->establishConnection('r');
$row = queryfx_one(
$conn,
'SELECT keyValue FROM %T WHERE keyName = %s',
$table->getTableName(),
$hmac_name);
if (!$row) {
return null;
}
return $row['keyValue'];
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sun, Jan 19, 19:24 (1 d, 20 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1127937
Default Alt Text
(83 KB)

Event Timeline