1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

//! Triggers a forced fdr by comparing the configured
//! index against the stored index

use anyhow::{format_err, Context as _, Error};
use fidl_fuchsia_recovery::{FactoryResetMarker, FactoryResetProxy};
use fidl_fuchsia_update_channel::{ProviderMarker, ProviderProxy};
use fuchsia_component::client::connect_to_protocol;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::fs::File;
use std::path::PathBuf;
use tracing::{info, warn};

const DEVICE_INDEX_FILE: &str = "stored-index.json";
const CONFIGURED_INDEX_FILE: &str = "forced-fdr-channel-indices.config";

#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "version", content = "content", deny_unknown_fields)]
enum ChannelIndices {
    #[serde(rename = "1")]
    Version1 { channel_indices: HashMap<String, i32> },
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "version", content = "content", deny_unknown_fields)]
enum StoredIndex {
    #[serde(rename = "1")]
    Version1 { channel: String, index: i32 },
}

struct ForcedFDR {
    data_dir: PathBuf,
    config_data_dir: PathBuf,
    info_proxy: ProviderProxy,
    factory_reset_proxy: FactoryResetProxy,
}

impl ForcedFDR {
    fn new() -> Result<Self, Error> {
        let info_proxy = connect_to_protocol::<ProviderMarker>()?;
        let factory_reset_proxy = connect_to_protocol::<FactoryResetMarker>()?;

        Ok(ForcedFDR {
            data_dir: "/data".into(),
            config_data_dir: "/config/data".into(),
            info_proxy,
            factory_reset_proxy,
        })
    }

    #[cfg(test)]
    fn new_mock(
        data_dir: PathBuf,
        config_data_dir: PathBuf,
    ) -> (
        Self,
        fidl_fuchsia_update_channel::ProviderRequestStream,
        fidl_fuchsia_recovery::FactoryResetRequestStream,
    ) {
        let (info_proxy, info_stream) =
            fidl::endpoints::create_proxy_and_stream::<ProviderMarker>().unwrap();
        let (fdr_proxy, fdr_stream) =
            fidl::endpoints::create_proxy_and_stream::<FactoryResetMarker>().unwrap();

        (
            ForcedFDR { data_dir, config_data_dir, info_proxy, factory_reset_proxy: fdr_proxy },
            info_stream,
            fdr_stream,
        )
    }
}

/// Performs a Factory Data Reset(FDR) on the device "if necessary."
///
/// Necessity is determined by comparing the index stored in
/// `forced-fdr-channel-indices.config` for the device's ota channel against
/// an index written into storage that represents the last successful FDR.
/// `forced-fdr-channel-indices.config` is provided on a per board basis using
/// config-data.
///
/// # Errors
///
/// There are numerous cases (config missing, failed to read file, ...)
/// where the library is unable to determine if an FDR is necessary. In
/// all of these cases, the library will *not* FDR.
pub async fn perform_fdr_if_necessary() {
    perform_fdr_if_necessary_impl().await.unwrap_or_else(|err| info!(tag = "forced-fdr", ?err))
}

async fn perform_fdr_if_necessary_impl() -> Result<(), Error> {
    let forced_fdr = ForcedFDR::new().context("Failed to connect to required services")?;
    run(forced_fdr).await
}

async fn run(fdr: ForcedFDR) -> Result<(), Error> {
    let current_channel =
        get_current_channel(&fdr).await.context("Failed to get current channel")?;

    let channel_indices = get_channel_indices(&fdr).context("Channel indices not available")?;

    if !is_channel_in_allowlist(&channel_indices, &current_channel) {
        return Err(format_err!("Not in forced FDR allowlist"));
    }

    let channel_index = get_channel_index(&channel_indices, &current_channel)
        .ok_or_else(|| format_err!("Not in forced FDR allowlist."))?;

    let device_index = match get_stored_index(&fdr, &current_channel) {
        Ok(index) => index,
        Err(err) => {
            info!(%err, "Unable to read stored index");
            // The device index is missing so it should be
            // written in preparation for the next FDR ota.
            // The index will always be missing right after
            // an FDR.
            return write_stored_index(&fdr, &current_channel, channel_index)
                .context("Failed to write device index");
        }
    };

    if device_index >= channel_index {
        return Err(format_err!("FDR not required"));
    }

    trigger_fdr(&fdr).await.context("Failed to trigger FDR")?;

    Ok(())
}

fn get_channel_indices(fdr: &ForcedFDR) -> Result<HashMap<String, i32>, Error> {
    let f = open_channel_indices_file(fdr)?;
    match serde_json::from_reader(std::io::BufReader::new(f))? {
        ChannelIndices::Version1 { channel_indices } => Ok(channel_indices),
    }
}

fn open_channel_indices_file(fdr: &ForcedFDR) -> Result<File, Error> {
    Ok(fs::File::open(fdr.config_data_dir.join(CONFIGURED_INDEX_FILE))?)
}

async fn get_current_channel(fdr: &ForcedFDR) -> Result<String, Error> {
    Ok(fdr.info_proxy.get_current().await?)
}

fn is_channel_in_allowlist(allowlist: &HashMap<String, i32>, channel: &String) -> bool {
    allowlist.contains_key(channel)
}

fn get_channel_index(channel_indices: &HashMap<String, i32>, channel: &String) -> Option<i32> {
    channel_indices.get(channel).copied()
}

async fn trigger_fdr(fdr: &ForcedFDR) -> Result<i32, Error> {
    warn!("Triggering FDR. SSH keys will be lost");
    Ok(fdr.factory_reset_proxy.reset().await?)
}

fn get_stored_index(fdr: &ForcedFDR, current_channel: &String) -> Result<i32, Error> {
    let f = open_stored_index_file(fdr)?;
    match serde_json::from_reader(std::io::BufReader::new(f))? {
        StoredIndex::Version1 { channel, index } => {
            // The channel has been changed, and thus nothing can be assumed.
            // Report error so the file will be replaced with the file for
            // the new channel.
            if *current_channel != channel {
                return Err(format_err!("Mismatch between stored and current channel"));
            }

            Ok(index)
        }
    }
}

fn open_stored_index_file(fdr: &ForcedFDR) -> Result<File, Error> {
    Ok(fs::File::open(fdr.data_dir.join(DEVICE_INDEX_FILE))?)
}

fn write_stored_index(fdr: &ForcedFDR, channel: &String, index: i32) -> Result<(), Error> {
    info!("Writing index {} for channel {}", index, channel);
    let stored_index = StoredIndex::Version1 { channel: channel.to_string(), index };
    let contents = serde_json::to_string(&stored_index)?;
    fs::write(fdr.data_dir.join(DEVICE_INDEX_FILE), contents)?;
    Ok(())
}

#[cfg(test)]
mod forced_fdr_test;