1 /* SPDX-License-Identifier: LGPL-2.1-or-later */
2 
3 #if HAVE_LIBFIDO2
4 #include <fido.h>
5 #endif
6 
7 #include "ask-password-api.h"
8 #include "errno-util.h"
9 #include "format-table.h"
10 #include "hexdecoct.h"
11 #include "homectl-fido2.h"
12 #include "homectl-pkcs11.h"
13 #include "libcrypt-util.h"
14 #include "libfido2-util.h"
15 #include "locale-util.h"
16 #include "memory-util.h"
17 #include "random-util.h"
18 #include "strv.h"
19 
20 #if HAVE_LIBFIDO2
add_fido2_credential_id(JsonVariant ** v,const void * cid,size_t cid_size)21 static int add_fido2_credential_id(
22                 JsonVariant **v,
23                 const void *cid,
24                 size_t cid_size) {
25 
26         _cleanup_(json_variant_unrefp) JsonVariant *w = NULL;
27         _cleanup_strv_free_ char **l = NULL;
28         _cleanup_free_ char *escaped = NULL;
29         int r;
30 
31         assert(v);
32         assert(cid);
33 
34         r = base64mem(cid, cid_size, &escaped);
35         if (r < 0)
36                 return log_error_errno(r, "Failed to base64 encode FIDO2 credential ID: %m");
37 
38         w = json_variant_ref(json_variant_by_key(*v, "fido2HmacCredential"));
39         if (w) {
40                 r = json_variant_strv(w, &l);
41                 if (r < 0)
42                         return log_error_errno(r, "Failed to parse FIDO2 credential ID list: %m");
43 
44                 if (strv_contains(l, escaped))
45                         return 0;
46         }
47 
48         r = strv_extend(&l, escaped);
49         if (r < 0)
50                 return log_oom();
51 
52         w = json_variant_unref(w);
53         r = json_variant_new_array_strv(&w, l);
54         if (r < 0)
55                 return log_error_errno(r, "Failed to create FIDO2 credential ID JSON: %m");
56 
57         r = json_variant_set_field(v, "fido2HmacCredential", w);
58         if (r < 0)
59                 return log_error_errno(r, "Failed to update FIDO2 credential ID: %m");
60 
61         return 0;
62 }
63 
add_fido2_salt(JsonVariant ** v,const void * cid,size_t cid_size,const void * fido2_salt,size_t fido2_salt_size,const void * secret,size_t secret_size,Fido2EnrollFlags lock_with)64 static int add_fido2_salt(
65                 JsonVariant **v,
66                 const void *cid,
67                 size_t cid_size,
68                 const void *fido2_salt,
69                 size_t fido2_salt_size,
70                 const void *secret,
71                 size_t secret_size,
72                 Fido2EnrollFlags lock_with) {
73 
74         _cleanup_(json_variant_unrefp) JsonVariant *l = NULL, *w = NULL, *e = NULL;
75         _cleanup_(erase_and_freep) char *base64_encoded = NULL, *hashed = NULL;
76         int r;
77 
78         /* Before using UNIX hashing on the supplied key we base64 encode it, since crypt_r() and friends
79          * expect a NUL terminated string, and we use a binary key */
80         r = base64mem(secret, secret_size, &base64_encoded);
81         if (r < 0)
82                 return log_error_errno(r, "Failed to base64 encode secret key: %m");
83 
84         r = hash_password(base64_encoded, &hashed);
85         if (r < 0)
86                 return log_error_errno(errno_or_else(EINVAL), "Failed to UNIX hash secret key: %m");
87 
88         r = json_build(&e, JSON_BUILD_OBJECT(
89                                        JSON_BUILD_PAIR("credential", JSON_BUILD_BASE64(cid, cid_size)),
90                                        JSON_BUILD_PAIR("salt", JSON_BUILD_BASE64(fido2_salt, fido2_salt_size)),
91                                        JSON_BUILD_PAIR("hashedPassword", JSON_BUILD_STRING(hashed)),
92                                        JSON_BUILD_PAIR("up", JSON_BUILD_BOOLEAN(FLAGS_SET(lock_with, FIDO2ENROLL_UP))),
93                                        JSON_BUILD_PAIR("uv", JSON_BUILD_BOOLEAN(FLAGS_SET(lock_with, FIDO2ENROLL_UV))),
94                                        JSON_BUILD_PAIR("clientPin", JSON_BUILD_BOOLEAN(FLAGS_SET(lock_with, FIDO2ENROLL_PIN)))));
95 
96         if (r < 0)
97                 return log_error_errno(r, "Failed to build FIDO2 salt JSON key object: %m");
98 
99         w = json_variant_ref(json_variant_by_key(*v, "privileged"));
100         l = json_variant_ref(json_variant_by_key(w, "fido2HmacSalt"));
101 
102         r = json_variant_append_array(&l, e);
103         if (r < 0)
104                 return log_error_errno(r, "Failed append FIDO2 salt: %m");
105 
106         r = json_variant_set_field(&w, "fido2HmacSalt", l);
107         if (r < 0)
108                 return log_error_errno(r, "Failed to set FDO2 salt: %m");
109 
110         r = json_variant_set_field(v, "privileged", w);
111         if (r < 0)
112                 return log_error_errno(r, "Failed to update privileged field: %m");
113 
114         return 0;
115 }
116 #endif
117 
identity_add_fido2_parameters(JsonVariant ** v,const char * device,Fido2EnrollFlags lock_with,int cred_alg)118 int identity_add_fido2_parameters(
119                 JsonVariant **v,
120                 const char *device,
121                 Fido2EnrollFlags lock_with,
122                 int cred_alg) {
123 
124 #if HAVE_LIBFIDO2
125         JsonVariant *un, *realm, *rn;
126         _cleanup_(erase_and_freep) void *secret = NULL, *salt = NULL;
127         _cleanup_(erase_and_freep) char *used_pin = NULL;
128         size_t cid_size, salt_size, secret_size;
129         _cleanup_free_ void *cid = NULL;
130         const char *fido_un;
131         int r;
132 
133         assert(v);
134         assert(device);
135 
136         un = json_variant_by_key(*v, "userName");
137         if (!un)
138                 return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
139                                        "userName field of user record is missing");
140         if (!json_variant_is_string(un))
141                 return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
142                                        "userName field of user record is not a string");
143 
144         realm = json_variant_by_key(*v, "realm");
145         if (realm) {
146                 if (!json_variant_is_string(realm))
147                         return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
148                                                "realm field of user record is not a string");
149 
150                 fido_un = strjoina(json_variant_string(un), json_variant_string(realm));
151         } else
152                 fido_un = json_variant_string(un);
153 
154         rn = json_variant_by_key(*v, "realName");
155         if (rn && !json_variant_is_string(rn))
156                 return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
157                                        "realName field of user record is not a string");
158 
159         r = fido2_generate_hmac_hash(
160                         device,
161                         /* rp_id= */ "io.systemd.home",
162                         /* rp_name= */ "Home Directory",
163                         /* user_id= */ fido_un, strlen(fido_un), /* We pass the user ID and name as the same */
164                         /* user_name= */ fido_un,
165                         /* user_display_name= */ rn ? json_variant_string(rn) : NULL,
166                         /* user_icon_name= */ NULL,
167                         /* askpw_icon_name= */ "user-home",
168                         lock_with,
169                         cred_alg,
170                         &cid, &cid_size,
171                         &salt, &salt_size,
172                         &secret, &secret_size,
173                         &used_pin,
174                         &lock_with);
175         if (r < 0)
176                 return r;
177 
178         r = add_fido2_credential_id(
179                         v,
180                         cid,
181                         cid_size);
182         if (r < 0)
183                 return r;
184 
185         r = add_fido2_salt(
186                         v,
187                         cid,
188                         cid_size,
189                         salt,
190                         salt_size,
191                         secret,
192                         secret_size,
193                         lock_with);
194         if (r < 0)
195                 return r;
196 
197         /* If we acquired the PIN also include it in the secret section of the record, so that systemd-homed
198          * can use it if it needs to, given that it likely needs to decrypt the key again to pass to LUKS or
199          * fscrypt. */
200         r = identity_add_token_pin(v, used_pin);
201         if (r < 0)
202                 return r;
203 
204         return 0;
205 #else
206         return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
207                                "FIDO2 tokens not supported on this build.");
208 #endif
209 }
210