Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
221 changes: 221 additions & 0 deletions class-two-factor-core.php
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@ public static function add_hooks( $compat ) {
add_action( 'login_enqueue_scripts', array( __CLASS__, 'login_enqueue_scripts' ), 5 );
add_action( 'admin_init', array( __CLASS__, 'trigger_user_settings_action' ) );
add_action( 'admin_init', array( __CLASS__, 'add_privacy_policy_content' ) );

// Personal data export and erasure tools.
add_filter( 'wp_privacy_personal_data_exporters', array( __CLASS__, 'register_personal_data_exporter' ) );
add_filter( 'wp_privacy_personal_data_erasers', array( __CLASS__, 'register_personal_data_eraser' ) );

add_filter( 'two_factor_providers', array( __CLASS__, 'enable_dummy_method_for_debug' ) );

// Add Settings link to plugin action links.
Expand Down Expand Up @@ -2963,4 +2968,220 @@ public static function add_privacy_policy_content() {
wp_kses_post( wpautop( $content, false ) )
);
}

/**
* Registers the personal data exporter.
*
* @since 0.17.0
*
* @param array $exporters List of personal data exporters.
* @return array
*/
public static function register_personal_data_exporter( $exporters ) {
$exporters['two-factor'] = array(
'exporter_friendly_name' => __( 'Two Factor Authentication Data', 'two-factor' ),
'callback' => array( __CLASS__, 'personal_data_exporter' ),
);

return $exporters;
}

/**
* Registers the personal data eraser.
*
* @since 0.17.0
*
* @param array $erasers List of personal data erasers.
* @return array
*/
public static function register_personal_data_eraser( $erasers ) {
$erasers['two-factor'] = array(
'eraser_friendly_name' => __( 'Two Factor Authentication Data', 'two-factor' ),
'callback' => array( __CLASS__, 'personal_data_eraser' ),
);

return $erasers;
}

/**
* Exports the Two Factor data stored for a user.
*
* Credentials are described, never included. The TOTP secret, the backup
* codes and the email token hash stay out of the export file so that it
* remains safe to share.
*
* @since 0.17.0
*
* @param string $email_address The email address of the user.
* @param int $page The page of data being requested.
* @return array
*/
public static function personal_data_exporter( $email_address, $page = 1 ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- Pagination is not needed, all data fits on one page.
$user = get_user_by( 'email', $email_address );

if ( ! $user ) {
return array(
'data' => array(),
'done' => true,
);
}

$data = array();

$enabled_providers = get_user_meta( $user->ID, self::ENABLED_PROVIDERS_USER_META_KEY, true );
if ( $enabled_providers ) {
$data[] = array(
'name' => __( 'Enabled Two Factor methods', 'two-factor' ),
'value' => implode( ', ', (array) $enabled_providers ),
);
}

$primary_provider = get_user_meta( $user->ID, self::PROVIDER_USER_META_KEY, true );
if ( $primary_provider ) {
$data[] = array(
'name' => __( 'Primary Two Factor method', 'two-factor' ),
'value' => $primary_provider,
);
}

$failed_attempts = get_user_meta( $user->ID, self::USER_FAILED_LOGIN_ATTEMPTS_KEY, true );
if ( $failed_attempts ) {
$data[] = array(
'name' => __( 'Failed Two Factor login attempts', 'two-factor' ),
'value' => $failed_attempts,
);
}

$last_failure = get_user_meta( $user->ID, self::USER_RATE_LIMIT_KEY, true );
if ( $last_failure ) {
$data[] = array(
'name' => __( 'Last failed Two Factor login', 'two-factor' ),
'value' => self::format_privacy_timestamp( $last_failure ),
);
}

foreach ( self::get_providers() as $provider ) {
$provider_data = $provider->privacy_export_data( $user );
Comment on lines +3063 to +3064

if ( ! empty( $provider_data ) ) {
$data = array_merge( $data, $provider_data );
}
}

if ( empty( $data ) ) {
return array(
'data' => array(),
'done' => true,
);
}

return array(
'data' => array(
array(
'group_id' => 'two-factor',
'group_label' => __( 'Two Factor Authentication', 'two-factor' ),
'group_description' => __( 'Two Factor authentication data for the user.', 'two-factor' ),
'item_id' => 'two-factor',
'data' => $data,
),
),
'done' => true,
);
}

/**
* Erases the Two Factor data stored for a user.
*
* Only the short-lived records are removed. The authentication credentials
* are kept, because the erasure tool does not delete the user account and
* removing the credentials would leave it protected by a password only.
*
* @since 0.17.0
*
* @param string $email_address The email address of the user.
* @param int $page The page of data being processed.
* @return array
*/
public static function personal_data_eraser( $email_address, $page = 1 ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- Pagination is not needed, all data fits on one page.
$user = get_user_by( 'email', $email_address );

if ( ! $user ) {
return array(
'items_removed' => false,
'items_retained' => false,
'messages' => array(),
'done' => true,
);
}

$meta_keys = array(
self::USER_META_NONCE_KEY,
self::USER_RATE_LIMIT_KEY,
self::USER_FAILED_LOGIN_ATTEMPTS_KEY,
self::USER_PASSWORD_WAS_RESET_KEY,
);

$retained_keys = array(
self::PROVIDER_USER_META_KEY,
self::ENABLED_PROVIDERS_USER_META_KEY,
);

foreach ( self::get_providers() as $provider ) {
$eraser_keys = $provider::privacy_eraser_user_meta_keys();

$meta_keys = array_merge( $meta_keys, $eraser_keys );

// Credentials the provider keeps are disclosed as retained.
$retained_keys = array_merge(
$retained_keys,
array_diff( $provider::uninstall_user_meta_keys(), $eraser_keys )
);
}

$items_removed = false;
foreach ( array_unique( $meta_keys ) as $meta_key ) {
if ( delete_user_meta( $user->ID, $meta_key ) ) {
$items_removed = true;
}
}

$items_retained = false;
foreach ( array_unique( $retained_keys ) as $meta_key ) {
if ( get_user_meta( $user->ID, $meta_key, true ) ) {
$items_retained = true;
break;
}
}

$messages = array();
if ( $items_retained ) {
$messages[] = __( 'Two Factor authentication credentials were retained because erasing them would remove the second factor from an account that still exists. They are removed when the user account is deleted.', 'two-factor' );
}

return array(
'items_removed' => $items_removed,
'items_retained' => $items_retained,
'messages' => $messages,
'done' => true,
);
}

/**
* Formats a timestamp for the export and erasure reports.
*
* @since 0.17.0
*
* @param int|string $timestamp Unix timestamp to format.
* @return string Formatted date and time, or an empty string when no timestamp is set.
*/
public static function format_privacy_timestamp( $timestamp ) {
if ( empty( $timestamp ) ) {
return '';
}

return wp_date(
get_option( 'date_format' ) . ' ' . get_option( 'time_format' ),
(int) $timestamp
);
}
}
29 changes: 29 additions & 0 deletions providers/class-two-factor-backup-codes.php
Original file line number Diff line number Diff line change
Expand Up @@ -533,4 +533,33 @@ public static function uninstall_user_meta_keys() {
self::BACKUP_CODES_META_KEY,
);
}

/**
* Return the personal data stored for a user for the exporter.
*
* The codes and their hashes are never included, only how many are left.
*
* @since 0.17.0
*
* @param WP_User $user WP_User object of the user.
* @return array
*/
public function privacy_export_data( $user ) {
$remaining = self::codes_remaining_for_user( $user );

if ( ! $remaining ) {
return array();
}

return array(
array(
'name' => __( 'Recovery codes', 'two-factor' ),
'value' => sprintf(
/* translators: %d: number of unused codes */
_n( '%d unused code', '%d unused codes', $remaining, 'two-factor' ),
$remaining
),
),
);
}
}
47 changes: 47 additions & 0 deletions providers/class-two-factor-email.php
Original file line number Diff line number Diff line change
Expand Up @@ -472,4 +472,51 @@ public static function uninstall_user_meta_keys() {
self::TOKEN_META_KEY_TIMESTAMP,
);
}

/**
* Return the user meta keys that the personal data eraser should delete.
*
* Both keys hold short-lived data about a pending code.
*
* @since 0.17.0
*
* @return array
*/
public static function privacy_eraser_user_meta_keys() {
return array(
self::TOKEN_META_KEY,
self::TOKEN_META_KEY_TIMESTAMP,
);
}

/**
* Return the personal data stored for a user for the exporter.
*
* The hashed token is never included, only when the code was sent.
* The timestamp outlives the token after the code is consumed, so it
* is reported on its own.
*
* @since 0.17.0
*
* @param WP_User $user WP_User object of the user.
* @return array
*/
public function privacy_export_data( $user ) {
$timestamp = (int) get_user_meta( $user->ID, self::TOKEN_META_KEY_TIMESTAMP, true );

if ( ! $timestamp ) {
return array();
}

return array(
array(
'name' => __( 'Email login code', 'two-factor' ),
'value' => sprintf(
/* translators: %s: date and time */
__( 'A code was sent on %s.', 'two-factor' ),
Two_Factor_Core::format_privacy_timestamp( $timestamp )
),
),
);
}
}
33 changes: 33 additions & 0 deletions providers/class-two-factor-provider.php
Original file line number Diff line number Diff line change
Expand Up @@ -211,4 +211,37 @@ public static function uninstall_user_meta_keys() {
public static function uninstall_options() {
return array();
}

/**
* Return the user meta keys that the personal data eraser should delete.
*
* Only keys holding short-lived data belong here. Keys holding credentials
* are kept, because the erasure tool does not delete the user account and
* removing them would leave the account protected by a password only.
*
* @since 0.17.0
*
* Note: this method doesn't have access to the instantiated provider object.
*
* @return array
*/
public static function privacy_eraser_user_meta_keys() {
return array();
}

/**
* Return the personal data that the provider stores for a user.
*
* Returns name-value pairs for the personal data exporter. Secrets and
* hashes must not be included, describe the credential instead so that
* the export file stays safe to share.
*
* @since 0.17.0
*
* @param WP_User $user WP_User object of the user.
* @return array
*/
public function privacy_export_data( $user ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- Base implementation keeps the provider interface signature but does not use the user.
return array();
}
}
Loading