diff --git a/frontend/src/api/types/enc_keys.ts b/frontend/src/api/types/enc_keys.ts new file mode 100644 index 000000000..ecf8b7911 --- /dev/null +++ b/frontend/src/api/types/enc_keys.ts @@ -0,0 +1,9 @@ +export interface EncKeyMigrateRequest { + /// Validation: PATTERN_ALNUM + key_id: string, +} + +export interface EncKeysResponse { + active: string, + keys: string[], +} \ No newline at end of file diff --git a/frontend/src/api/types/login_times.ts b/frontend/src/api/types/login_times.ts new file mode 100644 index 000000000..de4761813 --- /dev/null +++ b/frontend/src/api/types/login_times.ts @@ -0,0 +1,11 @@ +export interface Argon2ParamsResponse { + m_cost: number, + t_cost: number, + p_cost: number, +} + +export interface LoginTimeResponse { + argon2_params: Argon2ParamsResponse, + login_time: number, + num_cpus: number, +} \ No newline at end of file diff --git a/frontend/src/api/types/oidc.ts b/frontend/src/api/types/oidc.ts new file mode 100644 index 000000000..03356b727 --- /dev/null +++ b/frontend/src/api/types/oidc.ts @@ -0,0 +1,20 @@ +export type JwkKeyPairAlg = 'RS256' | 'RS384' | 'RS512' | 'EdDSA'; +export type JwkKeyPairType = 'RSA' | 'OKP'; + +export interface JWKSPublicKeyCerts { + kty: JwkKeyPairType, + alg: JwkKeyPairAlg, + // Ed25519 + crv?: string, + kid?: string, + // RSA + n?: string, + // RSA + e?: string, + // OCT + x?: string, +} + +export interface JWKSCerts { + keys: JWKSPublicKeyCerts[], +} \ No newline at end of file diff --git a/frontend/src/api/types/password_hashing.ts b/frontend/src/api/types/password_hashing.ts new file mode 100644 index 000000000..6c2ea9dcb --- /dev/null +++ b/frontend/src/api/types/password_hashing.ts @@ -0,0 +1,20 @@ +export interface PasswordHashTimesRequest { + /// Validation: min 500 + target_time: number, + /// Validation: min 32768 + m_cost?: number, + /// Validation: min 2 + p_cost?: number, +} + +export interface PasswordHashTime { + alg: string, + m_cost: number, + t_cost: number, + p_cost: number, + time_taken: number, +} + +export interface PasswordHashTimes { + results: PasswordHashTime[], +} \ No newline at end of file diff --git a/frontend/src/api/types/password_policy.ts b/frontend/src/api/types/password_policy.ts index 25c479dfa..917fffff5 100644 --- a/frontend/src/api/types/password_policy.ts +++ b/frontend/src/api/types/password_policy.ts @@ -1,3 +1,22 @@ +export interface PasswordPolicyRequest { + /// Validation: `8 <= length_min <= 128` + length_min: number, + /// Validation: `8 <= length_max <= 128` + length_max: number, + /// Validation: `1 <= include_lower_case <= 32` + include_lower_case?: number, + /// Validation: `1 <= include_upper_case <= 32` + include_upper_case?: number, + /// Validation: `1 <= include_digits <= 32` + include_digits?: number, + /// Validation: `1 <= include_special <= 32` + include_special?: number, + /// Validation: `1 <= valid_days <= 3650` + valid_days?: number, + /// Validation: `1 <= not_recently_used <= 10` + not_recently_used?: number, +} + export interface PasswordPolicyResponse { length_min: number, length_max: number, diff --git a/frontend/src/components/admin/AdminMain.svelte b/frontend/src/components/admin/AdminMain.svelte index 59bd1199d..86da01b83 100644 --- a/frontend/src/components/admin/AdminMain.svelte +++ b/frontend/src/components/admin/AdminMain.svelte @@ -11,7 +11,7 @@ import Clients from "./clients/Clients.svelte"; import Sessions from "./sessions/Sessions.svelte"; import Attr from "./userAttr/Attr.svelte"; - import Config from "./config/Config.svelte"; + import Config from "$lib5/admin/config/Config.svelte"; import IconWrenchScrew from "$lib/icons/IconWrenchScrew.svelte"; import IconUser from "$lib/icons/IconUser.svelte"; import IconOffice from "$lib/icons/IconOffice.svelte"; @@ -21,7 +21,7 @@ import IconLogout from "$lib/icons/IconLogout.svelte"; import IconId from "$lib/icons/IconId.svelte"; import RauthyLogo from "$lib/icons/RauthyLogo.svelte"; - import Documentation from "./documentation/Documentation.svelte"; + import Documentation from "$lib5/admin/documentation/Documentation.svelte"; import IconBookOpen from "$lib/icons/IconBookOpen.svelte"; import {onMount} from "svelte"; import Events from "$lib5/admin/events/Events.svelte"; diff --git a/frontend/src/components/admin/config/Config.svelte b/frontend/src/components/admin/config/Config.svelte deleted file mode 100644 index 520db73b4..000000000 --- a/frontend/src/components/admin/config/Config.svelte +++ /dev/null @@ -1,42 +0,0 @@ - - -
- This utility helps you find the best argon2id settings for your platform.
- Argon2id is currently the safest available password hashing algorithm. To use it to its fullest potential, it
- has
- to be tuned for each deployment.
-
- Important:These values need to be tuned on the final architecture!
- They change depending on the capabilities of the system. The more powerful the system, the more safe these
- values
- can be.
-
- If you want a detailed introduction to argon2id, many sources exist online. This guide will just give very short
- overview about the values.
- Three of them need to be configured:
-
- The m_cost
defines the amount of memory (in kB), which is used for the hashing.
- The higher the value, the better, of course. But you need to keep in mind the servers resources.
- When you hash 4 passwords at the same time, for instance, the backend needs 4 x m_cost
- during the
- hashing. These resources must be available.
-
- Tuning m_cost
is pretty easy. If you can give rauthy 1GB of memory for hashing operations,
- give it
- 1GB. If you can sacrifice 4GB, give it 4GB.
-
- Important: You should never go below an m_cost
of 32768
.
-
- The p_cost
defines the amount of parallelism for hashing.
- This value most often tops out at ~8, which is the default for Rauthy.
- p_cost
does not affect the time needed for the calculation, if the resources are available.
-
- The general rule is:
- Set the p_cost
to twice the size of cores your have available.
- For instance, if you have 4 cores available, set the p_cost
to 8
.
-
- The t_cost
defines the amount of time for hashing.
- This value is actually the only one, that needs tuning, since m_cost
and
- p_cost
are
- basically given by the environment.
-
- Tuning is easy: Set m_cost
and p_cost
accordingly and then increase t_cost
- as long as you have not reached your hashing-time-goal.
-
- Generally, users want everything as fast as possible. When doing a safe login though, a time between 500 - 1000
- ms
- should not be a problem.
- The login time must not be too short, since it would lower the strength of the hash, of course.
-
- To provide as much safety by default as possible, this utility does not allow you to go below 500 ms for the - login - time. -
-- The current values from the backend are the following: -
-
- Note: The Login Time from the backend does only provide a good guideline after at least 5 successful
- logins, after
- rauthy has been started.
- The base value is always 2000 ms after a fresh restart and will adjust over time with each successful login.
-
- You can use this tool to approximate good values for your deployment.
- Keep in mind, that this should be executed with rauthy in its final place with all final resources
- available.
- You should execute this utility during load to not over tune.
- m_cost
is optional and the safe minimal value of 32768
would be chosen, if empty.
- p_cost
is optional and rauthy will utilize all threads it can see, if empty.
-
- These Keys are used for an additional encryption at rest, independently from any data store technology used - under - the hood. They are configured statically, but can be rotated and migrated on this page manually to maybe get - rid - of an old key, which is currently still in use in some places. -
- -
- The active key is statically set in the Rauthy config file / environment variables. It cannot be changed
- here
- dynamically.
- All new JWK encryption's will always use the currently active key.
-
- If you migrate all existing secrets, it might take a few seconds to finish, if you have a big
- dataset.
- Manually migrate all existing encryption in the backend to key:
-
- These are the Json Web Keys (JWKs) used for token singing. -
- -
- You can rotate them and generate a full new set. Depending on your deployment, this could take a few
- seconds.
- New tokens will always be signed with the new / latest ones. The old keys will be cleaned up automatically,
- when
- there cannot be a token anymore that used the old key to not
- break any current token validation.
-
- Configure the password policy.
- The policy is being applied to all passwords being set from this moment on.
-
- Validity for a new password.
- Not Recently Used denied the last n used passwords.
- Setting no value at all with disable the given option.
-
- For general documentation about Rauthy itself, you should take a look at the - Rauthy Book -
- -
- If you want to integrate an external application and use Rauthy's REST API, take a look at the
- Swagger UI
-
- Note:
- Depending on the backend configuration, the Swagger UI may not be exposed publicly at this point.
- It is however by default available via the internal /metrics
HTTP server to not expose any information.
-
- The source code can be found in - Github -
-m_cost
definiert die Menga an Speicher (in kB) die zum Hashing verwendet
+ wird. Je höher dieser Wert, umso besser (sicherer), aber die notwendigen Ressourcen müssen natürlich
+ vorhanden sein.4 x m_cost
+ an Speicher benötigt, was zu jeder Zeit zur Verfügung stehen muss.`,
+ mCost2: `Den "richtigen" Wert für m_cost
zu finden ist glücklicherweise sehr einfach. Definiere
+ das Maximum an Speicher, das Rauthy nutzen sollte, dividiere die Menge durch die Anzahl paralleler Logins,
+ die möglich sein sollten (MAX_HASH_THREADS
) und ziehe hier von eine gewisse statische Menge ab.
+ Die Höhe des statisch benötigten Speichers hängt von der gewählten Datenbank und Anzahl Benutzer ab, jedoch
+ wird sie in den meisten Fällen im Bereich von 32 - 96 MB sein.`,
+
+ pCost1: `p_cost
definiert den Parallelismus fürs Hashing.p_cost
auf den zweifachen Wert der verfügbares CPU Kerne.p_cost
von 8 ein guter Wert.MAX_HASH_THREADS
)
+ berücksichtigen und ggf. entsprechend reduziert werden.`,
+
+ tCost1: `t_cost
ist ein Multiplikator für die Zeit fürs Hashing. Dies ist der einzige
+ Wert, der durch Testen auf der Zielarchitektur gefunden werden muss, weil m_cost
und
+ p_cost
gewissenermaßen vorgegeben sind.`,
+ tCost2: `Das Finden des Wertes ist einfach: Setze m_cost
und p_cost
wie oben
+ erklärt und erhöhe t_cost
so lange, bis die gewünschte Login Zeit erreicht wird.`,
+
+ utilityHead: 'Parameter Berechnungs-Werkzeug',
+ utility1: `Das folgende Werkzeug kann zum Finden passender Werte für dieses Rauthy deployment genutzt
+ werden. Da die Werte von sehr vielen Faktoren abhängen, sollten dieser auf der finalen Architektur
+ eingestellt werden, am besten zu Zeiten der am höchsten erwarteten Last, um keine zu hohen Werte
+ einzustellen.`,
+ utility2: `m_cost
ist Optional und der als minimal sichere Wert von 32768 würde automatisch
+ gewählt werden. Sollte p_cost
ebenfalls nicht gegeben sein, so wird Rauthy die maximal
+ verfügbare Menge and Kernen nutzen.`,
+
+ time: "Zeit",
+ targetTime: "Ziel-Zeit",
+ tune: 'Wichtig: Diese Werten müssen auf der finalen Architektur eingestellt werden!',
+ pDetials: `Für eine detailiertere Einführung in den Argon2ID Alrogithmus stehen vielen Quellen online zur
+ Verfügung. Hier werden nur ganz kurz die Werte erklärt. Die folgenden drei Werte müssen konfiguriert werden:`,
+ pTune: `Die Werte können stark variieren in Abhängigkeit vom System und der generellen Systemlast. Je
+ stärker das System, desto sicherere Werte können gewählt werden.`,
+ pUtility: `Dieses Werkzeug ist eine Hilfe zum Finden der besten Argon2ID Werte für das jeweilige System.
+ Argon2ID is der derzeit sicherste, verfügbare Passwort Hashing Algorithmus. Um das volle Potential
+ ausschöpfen zu können, müssen die Werte allerdings auf das System angepasst werden.`,
+ mCost3: "Der minimal erlaubte Wert für m_cost
ist 32768
."
+ },
+ openapi: "Zur Integration einer externen Applikation via Rauthy's API gibt es das",
+ openapiNote: `In Abhängigkeit von der Konfiguration ist das Swagger UI nicht öffentlich zugänglich übber den
+ oben genannten Link. Es ist allerdings (standardmäßig) über den internen metrics server verfügbar zur
+ Reduzierung der Angriffsfläche.`,
+ source: "Der source code kann hier gefunden werden",
+ },
error: {
needsAdminRole: 'Um Zugriff zu erhalten ist die Rolle rauthy_admin notwendig.',
noAdmin: `Für Rauthy Admin Accounts ist MFA Pflicht.m_cost
defines the amount of memory (in kB), which is used for the hashing.
+ The higher the value, the better, of course. But you need to keep in mind the servers resources.4 x m_cost
+ during the hashing. These resources must be available.`,
+ mCost2: `Tuning m_cost
is pretty easy. Define the max amount of memory that Rauthy should use,
+ divide it by the number of max allowed parallel logins (MAX_HASH_THREADS
) and subtract a small
+ static amount of memory. How much static memory should be taken into account depends on the used database
+ and the total amount of users, but will typically be in the range of 32 - 96 MB.`,
+ mCost3: 'The minimal allowed m_cost
is 32768
.',
+
+ pCost1: `The p_cost
defines the amount of parallelism for hashing. This value most often
+ tops out at ~8, which is the default for Rauthy.`,
+ pCost2: `The general rule is:p_cost
to twice the size of cores your have available.p_cost
to 8
.MAX_HASH_THREADS
) into
+ account and be reduced accordingly.`,
+
+ tCost1: `The t_cost
defines the amount of time for hashing. This value is actually the
+ only value, that needs tuning, since m_cost
and p_cost
are basically given by the
+ environment.`,
+ tCost2: `Tuning is easy: Set m_cost
and p_cost
accordingly and then increase
+ t_cost
as long as you have not reached your hashing-time-goal.`,
+
+ utilityHead: 'Parameter Calculation Utility',
+ utility1: `You can use this tool to approximate good values for your deployment. Keep in mind, that this
+ should be executed with Rauthy in its final place with all final resources available. You should execute
+ this utility during load to not over tune.`,
+ utility2: `m_cost
is optional and the safe minimal value of 32768
would be chosen,
+ if empty. p_cost
is optional too and Rauthy will utilize all threads it can see, if empty.`,
+
+ time: "Time",
+ targetTime: "Target Time",
+ tune: 'Important: These values need to be tuned on the final architecture!',
+ pDetials: `If you want a detailed introduction to Argon2ID, many sources exist online. This guide just
+ gives very short overview about the values. Three of them need to be configured:`,
+ pTune: `They change depending on the capabilities of the system. The more powerful the system, the more safe
+ these values can be.`,
+ pUtility: `This utility helps you find the best Argon2ID settings for your platform.
+ Argon2ID is currently the safest available password hashing algorithm. To use it to its fullest potential,
+ it has to be tuned for each deployment.`,
+ },
+ openapi: "If you want to integrate an external application and use Rauthy's API, take a look at the",
+ openapiNote: `Depending on the backend configuration, the Swagger UI may not be exposed publicly at this point.
+ It is however by default available via the internal metrics HTTP server to not expose any
+ information.`,
+ source: "The source code can be found here",
+ },
error: {
needsAdminRole: `You are not assigned to the rauthy_admin role.{ta.docs.encKeys.p1}
+{ta.docs.encKeys.p2}
+ ++ + {ta.docs.encKeys.keyActive}: + + + {activeKey} + +
+ ++ {ta.docs.encKeys.keysAvailable}: +
+ +{ta.docs.encKeys.p3}
+{ta.docs.encKeys.migrateToKey}:
+ +{ta.jwks.p1}
+{ta.jwks.p2}
+{ta.jwks.p3}
+ +{#each certs as jwk (jwk.kid)} ++ {ta.passwordPolicy.configDesc} +
+ + + + diff --git a/frontend/src/lib_svelte5/admin/config/argon2/Argon2Params.svelte b/frontend/src/lib_svelte5/admin/config/argon2/Argon2Params.svelte new file mode 100644 index 000000000..a78e08455 --- /dev/null +++ b/frontend/src/lib_svelte5/admin/config/argon2/Argon2Params.svelte @@ -0,0 +1,112 @@ + + +{ta.docs.hashing.pUtility}
+
+ {ta.docs.hashing.tune}
+ {ta.docs.hashing.pTune}
+
{ta.docs.hashing.pDetials}
+{@html ta.docs.hashing.mCost1}
+{@html ta.docs.hashing.mCost2}
+{@html ta.docs.hashing.mCost3}
+{@html ta.docs.hashing.pCost1}
+{@html ta.docs.hashing.pCost2}
+{@html ta.docs.hashing.tCost1}
+{@html ta.docs.hashing.tCost2}
+{ta.docs.hashing.loginTime1}
+{ta.docs.hashing.loginTime2}
+{ta.docs.hashing.currValues1}
+{ta.docs.hashing.currValuesNote}
+ {ta.docs.hashing.currValuesThreadsAccess}: {numCpus}{ta.docs.hashing.utility1}
+{@html ta.docs.hashing.utility2}
+ +{#if params} ++ {ta.docs.book} + Rauthy Book +
+ +
+ {ta.docs.openapi}
+ Swagger UI
+
+ {ta.common.note}:
+ {ta.docs.openapiNote}
+
+ {ta.docs.source}: + Github +
+Values from the ID token after a successful upstream login can be mapped automatically.
The path
needs to be given in a regex like syntax. It can resolve to
- single JSON values or resolve to a value in a JSON object or array.
$.
marks the start of the JSON object
*
can be used as a wildcard in your path
$.roles
would target {"roles": "value"}
$.roles.*
can target a value inside an object or array like
{"roles": ["value", "notMyValue"]}
The authentication method to use on the /token
endpoint.
Most providers should work with basic
, some only with post
.
+import"../chunks/disclose-version.BDr9Qe-U.js";import"../chunks/legacy.DtyiMpWz.js";import{p as he,aJ as wt,c as p,F as It,r as m,s as l,t as oe,a as _e,aN as Et,i as L,ao as Pe,h as n,g as e,a9 as Ue,f as te,aq as h}from"../chunks/index-client.DAoU_hDn.js";import{k as Ct,s as ve}from"../chunks/render.mNhspeV3.js";import{e as it}from"../chunks/each.DTG73tix.js";import{a as o,t as C,d as f,e as Ge}from"../chunks/template.DcxtE4ym.js";import{p as d}from"../chunks/proxy.D3ASEzk3.js";import{i as M}from"../chunks/if.BANCODOf.js";import{p as se,r as yt}from"../chunks/props.VJ8UyA45.js";import{r as ke}from"../chunks/legacy-client.rYJ80fHr.js";import{E as lt}from"../chunks/ExpandContainer.mM6ZiTQe.js";import{c as He,a as A,I as S}from"../chunks/Input.C04-Kl2S.js";import{n as ae,$ as nt,a0 as ot,a1 as je,o as Ye}from"../chunks/helpers.BV-akmwW.js";import{B as fe}from"../chunks/Button.CKHLXer8.js";import{y as kt,z as At,A as Rt,B as Tt,C as Nt,D as Lt,E as St}from"../chunks/dataFetchingAdmin.D7RWqeRL.js";import{S as ie,P as ct}from"../chunks/PasswordInput.JBBIMWuW.js";import{C as dt}from"../chunks/CheckIcon.CrU43zqd.js";import{O as Dt,T as ut}from"../chunks/OptionSelect.CBlj_bre.js";import{s as Ot}from"../chunks/snippet.DHnbbgSs.js";import{s as Me,d as xt}from"../chunks/class.DWa3OhYO.js";import{s as Ae,t as Re,a as Te}from"../chunks/index.DER1jHiU.js";import{g as vt}from"../chunks/helpers.mrWtKrwW.js";import{T as Pt}from"../chunks/TabBar.BqcS880p.js";import{I as Ut}from"../chunks/ImageUploadRaw.BJmDAa-L.js";var Mt=C(`
Values from the ID token after a successful upstream login can be mapped automatically.
The path
needs to be given in a regex like syntax. It can resolve to
+ single JSON values or resolve to a value in a JSON object or array.
$.
marks the start of the JSON object
*
can be used as a wildcard in your path
$.roles
would target {"roles": "value"}
$.roles.*
can target a value inside an object or array like
{"roles": ["value", "notMyValue"]}
The authentication method to use on the /token
endpoint.
Most providers should work with basic
, some only with post
.
In rare situations, you need both, while it can lead to errors with others.
You can map a user to be a rauthy admin depending on an upstream ID claim.
If your provider issues a claim indicating that the user has used at least 2FA during - login, you can specify the mfa claim path.
The authentication method to use on the /token
endpoint.
Most providers should work with basic
, some only with post
.
+ login, you can specify the mfa claim path.
The authentication method to use on the /token
endpoint.
Most providers should work with basic
, some only with post
.
In rare situations, you need both, while it can lead to errors with others.
You can map a user to be a rauthy admin depending on an upstream ID claim.
If your provider issues a claim indicating that the user has used at least 2FA during - login, you can specify the mfa claim path.
"),na=C(`
This provider is in use by active users.
You can force delete it, but users without a local password or passkey - will not be able to log in anymore.
Are you sure you want to delete this provider?
"),na=C(`
This provider is in use by active users.
You can force delete it, but users without a local password or passkey + will not be able to log in anymore.
Are you sure you want to delete this provider?
Custom mappings cannot be done for OpenID default scopes and their names cannot be changed.
You can map custom scopes to attributes.
All additional attributes, that were configured, can have a custom value for each user.
When they are mapped to a scope, they can be included in the Access and / or ID Tokens.
chrome://flags/#fedcm-idp-registration
",1),Ie=I('
chrome://flags/#fedcm-idp-registration
",1),Ie=I('
You need to enable Cookies.
Without them, a safe login cannot be executed.
",1),Le=d(''),ke=d('
This window shows up during local dev,
only to be able to switch modes easily.
You need to enable Cookies.
Without them, a safe login cannot be executed.
",1),Ae=d(''),Le=d('
This window shows up during local dev,
only to be able to switch modes easily.
',1),Jt=I(" ",1),Gt=I('
"),Zt=I('
",1),wr=I(''),br=I('
',1),Bt=$(" ",1),Ot=$('
"),Xt=$('
",1),hr=$(''),wr=$('
An API Key must be provided in the HTTP Authorization
header in the following format:
API-Key YourSecretApiKeyHere
You can use the following curl
request to test your new Key:
If you don't have jq
installed and the above fails:
You Can generate a new secret for this API Key here.
You will only see this secret once after the generation. - When a new one has been generated, the old secret will be overridden permanently. - This operation cannot be reverted!
An API Key must be provided in the HTTP Authorization
header in the following format:
API-Key YourSecretApiKeyHere
You can use the following curl
request to test your new Key:
If you don't have jq
installed and the above fails:
You Can generate a new secret for this API Key here.
You will only see this secret once after the generation. + When a new one has been generated, the old secret will be overridden permanently. + This operation cannot be reverted!
CAUTION: The FORCE MFA
option for rauthy
itself is managed statically via the ADMIN_FORCE_MFA
config variable and will be overridden with each restart.
CAUTION: If you FORCE MFA
for this client here, this will only apply to the authorization_code
flow! If you use other flows,
- MFA cannot be forced for them!
rauthy
is the default client which is needed for logging into this Admin UI.
Be VERY careful when you change values here, since you could end up locking yourself out of the UI.
The Name can be changed without any impact on the configuration.
It will just show up on the Login / Logout screens.
Information about this client's URI and some contacts. - Client URI and Contacts might be shown to users on the login page.
Client configuration
Allowed Scopes will be applied, if the client asks for them during the login.
Default Scopes will always be applied.
Redirect URIs may contain a *
wildcard only at the end.
The Token Lifetime applies to the Access and ID tokens and is specified in seconds.
If your client does support EdDSA / ed25519 token algorithms, you should always use it for better security
- and
- smaller tokens.
The RSA algorithms does exist for compatibility reasons only.
The algorithm for the optional refresh token cannot be changed, since this should only be used by rauthy anyway.
If your application does support it, you should always use S256 PKCE challenges.
If you login from a single page application directly without any backend and therefore have a
- non-confidential
- client, you must(!) use at least the plain PKCE challenge, to have a secure login flow.
If any of these is set, rauthy will enforce the usage and deny any login, which does not provide a - valid challenge.
You can set client specific colors, which then will be used for the Login page. - These colors must be valid CSS values. Either enter them directly or use the color picker.