ssi/
runtime.rs

1// Self-sovereign identity
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Written in 2024 by
6//     Dr Maxim Orlovsky <orlovsky@lnp-bp.org>
7//
8// Copyright (C) 2024 LNP/BP Standards Association. All rights reserved.
9//
10// Licensed under the Apache License, Version 2.0 (the "License");
11// you may not use this file except in compliance with the License.
12// You may obtain a copy of the License at
13//
14//     http://www.apache.org/licenses/LICENSE-2.0
15//
16// Unless required by applicable law or agreed to in writing, software
17// distributed under the License is distributed on an "AS IS" BASIS,
18// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19// See the License for the specific language governing permissions and
20// limitations under the License.
21
22pub const SSI_DIR: &str = "~/.ssi";
23
24use std::collections::{BTreeSet, HashSet};
25use std::fs;
26use std::io::{self, BufRead, Write};
27use std::os::unix::fs::PermissionsExt;
28use std::path::PathBuf;
29
30use baid64::Baid64ParseError;
31
32use crate::{Fingerprint, SecretParseError, Ssi, SsiPair, SsiParseError, SsiQuery, SsiSecret};
33
34#[derive(Debug, Display, Error, From)]
35#[display(inner)]
36pub enum Error {
37    #[from]
38    Io(io::Error),
39
40    #[from]
41    Baid64(Baid64ParseError),
42
43    #[from]
44    Secret(SecretParseError),
45
46    #[from]
47    Ssi(SsiParseError),
48}
49
50pub struct SsiRuntime {
51    pub secrets: BTreeSet<SsiSecret>,
52    pub identities: HashSet<Ssi>,
53}
54
55impl SsiRuntime {
56    pub fn load() -> Result<Self, Error> {
57        let data_dir = PathBuf::from(shellexpand::tilde(SSI_DIR).to_string());
58        fs::create_dir_all(&data_dir)?;
59
60        let mut path = data_dir.clone();
61        path.push("secrets");
62        let file = fs::OpenOptions::new()
63            .read(true)
64            .write(true)
65            .create(true)
66            .truncate(false)
67            .open(path)?;
68        let mut permissions = file.metadata()?.permissions();
69        permissions.set_mode(0o600);
70        let reader = io::BufReader::new(file);
71        let mut secrets = bset![];
72        for line in reader.lines() {
73            let line = line?;
74            secrets.insert(line.parse()?);
75        }
76
77        let mut path = data_dir.clone();
78        path.push("identities");
79        let file = fs::OpenOptions::new()
80            .read(true)
81            .write(true)
82            .create(true)
83            .truncate(false)
84            .open(path)?;
85        let mut permissions = file.metadata()?.permissions();
86        permissions.set_mode(0o600);
87        let reader = io::BufReader::new(file);
88        let mut identities = set![];
89        for line in reader.lines() {
90            let line = line?;
91            identities.insert(line.parse()?);
92        }
93
94        Ok(Self {
95            secrets,
96            identities,
97        })
98    }
99
100    pub fn store(&self) -> io::Result<()> {
101        let data_dir = PathBuf::from(shellexpand::tilde(SSI_DIR).to_string());
102        fs::create_dir_all(&data_dir)?;
103
104        let mut path = data_dir.clone();
105        path.push("secrets");
106        let mut file = fs::File::create(path)?;
107        for secret in &self.secrets {
108            writeln!(file, "{secret}")?;
109        }
110
111        let mut path = data_dir.clone();
112        path.push("identities");
113        let mut file = fs::File::create(path)?;
114        for ssi in &self.identities {
115            writeln!(file, "{ssi}")?;
116        }
117
118        Ok(())
119    }
120
121    pub fn find_identity(&self, query: impl Into<SsiQuery>) -> Option<&Ssi> {
122        let query = query.into();
123        self.identities.iter().find(|ssi| match query {
124            SsiQuery::Pub(pk) => ssi.pk == pk,
125            SsiQuery::Fp(fp) => ssi.pk.fingerprint() == fp,
126            SsiQuery::Id(ref id) => ssi.uids.iter().any(|uid| {
127                &uid.id == id ||
128                    &uid.to_string() == id ||
129                    &uid.name == id ||
130                    &format!("{}:{}", uid.schema, uid.id) == id
131            }),
132        })
133    }
134
135    pub fn find_signer(&self, query: impl Into<SsiQuery>, passwd: &str) -> Option<SsiPair> {
136        let ssi = self.find_identity(query.into()).cloned()?;
137        let sk = self.secrets.iter().find_map(|s| {
138            let mut s = (*s).clone();
139            if !passwd.is_empty() {
140                s.decrypt(passwd);
141            }
142            if s.to_public() == ssi.pk {
143                Some(s)
144            } else {
145                None
146            }
147        })?;
148        Some(SsiPair::new(ssi, sk))
149    }
150
151    pub fn is_signing(&self, fp: Fingerprint) -> bool {
152        self.secrets.iter().any(|s| s.fingerprint() == fp)
153    }
154}