Skip to content
This repository has been archived by the owner on Sep 20, 2023. It is now read-only.

Option for users to only register with an invite link. #30

Closed
ghost opened this issue Mar 23, 2022 · 14 comments
Closed

Option for users to only register with an invite link. #30

ghost opened this issue Mar 23, 2022 · 14 comments
Labels
enhancement New feature or request
Milestone

Comments

@ghost
Copy link

ghost commented Mar 23, 2022

Option for new users to only be able to register with a generated invite link.

@mickvandijke mickvandijke added the enhancement New feature or request label May 2, 2022
@mickvandijke mickvandijke added this to the v1.1.0 milestone May 2, 2022
@da2ce7
Copy link
Contributor

da2ce7 commented May 28, 2022

Proposal for invitation specification:

  • Invitations are generated by users.
  • Invitations linked to the account that generated them.
  • Invitation may be used a configurable number of times.
  • Invitation have a validity start and finish date times.
  • Upon successful registration, the new user's account is linked to the account that made the invitation
  • User accounts have a configurable number of invited user slots. Once full, the invitations will stop to work.
  • User accounts can revoke invitations they have previously generated.

There is NO secret data that is stored on the server for making a Invitation.
A invitation is NOT supplied by a web URL. Instead it is a prefixed code.

Data Types:

 4Byte Unique User ID:
       Generated By Server upon User Registration.

16Byte Invitation Secret Token:
       Randomly Generated by User.
       Private to Invitation. Sent to server for Verification when token is Used. 

16Byte Token Salt:
       Randomly Generated by Server. Returned to User In-Pre Registration of Token.
       Used to stop reuse of the same `Invitation Hashed Token`

16Byte Invitation Hashed Token:
       Generated by User. Registered with Server. Used by server to Verify Tokens.
       Blake3 Hashed: (Invitation Secret Token || Unique User ID || Token Hashed Salt) -> 16Byte Output.

16Byte Token ID Salt:
       Randomly Generated by Server. Returned to User after Registering Token.

 8Byte Token ID:
       Generated by Server and User.
       Blake3 Hashed: (Invitation Hashed Token || Unique User ID || Token ID Salt) -> 8Byte Output.

12Byte Token Index:
       Index for Token's in Server Database.
       Form: Unique User ID || Token ID

 8Byte Token Start Time:
       Unix 64bit Time. Selected by User. Registered with Server.

 8Byte Token End Time:
       Unix 64bit Time. Selected by User. Registered with Server.

 4Byte Maximum Use Count:
       Maximum number of times this token can be used. Selected by User. Registered with Server.

 4Byte Token Use Count:
       Kept track by Server.

28Byte Invitation Payload:
       Unique User ID || Token ID || Invitation Secret Token

Making a Token.

  1. User asks to Server to Start the New Invitation Token Process.
  2. Server Generates, Stores, and Returns Token Salt.
  3. User Generates Invitation Secret Token and Derives the Invitation Hashed Token.
  4. User Registers the New Invitation Token with the Server, Sending:
  • Token Salt
  • Invitation Hashed Token
  • Token Start Time
  • Token End Time
  • Maximum Use Count
  1. Server Generates, Stores, and Returns the Token ID Salt.
  2. Server Derives the Token ID and uses it as the index for the User's Invitation Tokens.
  3. User Derives the Token ID, and formats the Invitation Payload.
  4. User Bech32m Encodes the Invitation Payload with the correct Human Readable Part.

Using a Token.

  1. User decodes the Invitation Payload from the Bech32m token.
  2. User sends the to the server:
  • Unique User ID
  • Token ID
  • Invitation Secret Token
  1. The Server looks up the User ID, Finds the Correct Token ID, and gets the Invitation Hashed Token
  2. The Server compares the computed Invitation Hashed Token with the version on file.
  3. The Server accepts the token as valid if the same.
  4. The Server checks the associated Token Start Time, ´Token End Time, and Maximum Use Count, and Token Use Count`
  5. The Server checks if the user has any invitation slots left. And Locks a Slot for the Time taken to register.
  6. The Server allows the user to register.

Upon Successful registration the server allocates:

  • The new user to the Locked Invitation Slot.
  • Iterates the Associated Token's Token Use Count

If the Registration somehow fails or has a time-out, the Server unlocks the Invitation Slot

Rust Blake3: https://docs.rs/blake3/latest/blake3/
JavaScript: https://github.com/connor4312/blake3
JavaScript Bech32m: https://github.com/bitcoinjs/bech32


A Invitation-code is encoded with: Bech32m.
It has the following form:

  • Human Readable Part:
    Configurable "short-name" of the tracker, such as: 'irontracker', or 'supernest' etc.
  • The Separator:
    Standard to use the character One ’1’
    The Payload is the 28Byte Invitation Payload

The Bech32m Encoding is both Encoded and Decoded on the Fronted.

@da2ce7
Copy link
Contributor

da2ce7 commented May 28, 2022

@WarmBeer and @BelieveInBunny

I have written a proposal for the protocol for the Invitations. This protocol avoids the server storing any secrets for the creation of invitations.

@BelieveInBunny
Copy link

@WarmBeer and @BelieveInBunny

I have written a proposal for the protocol for the Invitations. This protocol avoids the server storing any secrets for the creation of invitations.

it is a good idea. I will help to implement it like this

@da2ce7
Copy link
Contributor

da2ce7 commented May 29, 2022

@BelieveInBunny I have updated the proposal to assure that a hashed token cannot be reused in multiple invitations. (using a server supplied salt).

@da2ce7
Copy link
Contributor

da2ce7 commented May 29, 2022

@WarmBeer, @BelieveInBunny, @george-avn

My first protocol proposal works in a similar way as hashed passwords, the server doesn't store any passwords directly, but only a hashed image of the password (appropriately salted of-course). If the hashed-password database was public, nobody would be able to instantly log-in because to authenticate one must provide the pre-image to the hashed password i.e. (password || salt).

The limitation of this approach is that for every user-secret there needs to have an associated hashed image that is registered on the server. Meaning that the user must inform the server about any new tokens.

The naïve approach is to instead to store a secret on the server. To generate a new token, a secret and token iterator are hashed encrypted, i.e (hash(secret || nonce). The user then gives the token-bearer the hashed secret and the nonce. When the token-bearer supplies this to the server, the sever can hash the nonce with the stored secret and check the validity of the encrypted hashed token.

This approach allows the user to generate new tokens independently without coordination with the server. i.e. offline. The problem is that we have moved back to the original insecurity of the server holding secrets, anyone with access to the servers secret store can impersonate any token they want.

The correct approach to allow for independent token generation is to use cryptographic signatures. The users registers their public key with the server. To generate a token the user signs the token with their private key, producing a secure signature. The token-bearer bring this signed token to the server, who verifies the signature against the pre-registered public key.

This approach has none of the downfalls of the other approaches, tokens can be generated independently of the sever, and there is no secret data that is stored inside the server's databases. The problem is that is slightly harder to implement, and the tokens are a little larger. (maybe 80bytes instead of 30bytes).

I am considering to write another protocol that uses the cryptographic signature approach, as it is much more future-proof; and the allowance for independent token generation is very useful. (Another 'Invitation Server' could be set-up, and generate tokens based upon it's own policy; it would not need to communicate with any other server).

@BelieveInBunny
Copy link

BelieveInBunny commented May 29, 2022

it is a bit difficult to implement but a better way to approach, I will start work on this.

should I work on this first or finish other milestone 1.1.0 first?
(e.g. #11
#49 )

@da2ce7
Copy link
Contributor

da2ce7 commented May 29, 2022

@BelieveInBunny Great! I will write the signed invitation specification, until then work on the other tasks. 👍

@BelieveInBunny
Copy link

Noted with thanks

@da2ce7
Copy link
Contributor

da2ce7 commented May 29, 2022

Signed Invitation Codes

This is an alternative specification for making Invitation Codes. This specification has a specific advantage on-top-of the Normal Invitation Codes: Non-Interactive Code Generation.

Non-Interactive Code Generation

No-interactive code generation allows for a user to create new invitation codes without any additional communication with the server. This would allow for example an invitation server or bot to give out codes without needing to continually connect to the Torrust Backend API.

Signed Invitations Overview

  • User Generates a Private/Public Keypair.
  • User Registers their Public Key with the Server.
  • User Makes New Invitations by Signing an Invitation Message.
  • The Server verify the signature of Signing an Invitation against the Registered Public Key for that User.

Technology Used

Using standard schnorr signatures for secp256k1. Used by Bitcoin in the recent Taproot Upgrade.

BIP340 - Schnorr Signatures for secp256k1 (The Signing Algorithm)
libsecp256k1 - C Implementation of Signing Algorithm.
rust-secp256k1 - Rust-Binding to libsecp256k1. (For Backend Signature Verification)
noble-secp256k1 - TypeScript Implementation secp256k1. (For Frontend Signature Verification)
bip-0350 - The specification for bech32m.
bech32 - TypeScript implementation of Bech32m (For Encoding the Invitation)

Specification

Introduction

Users of Torrust should easily be able to invite their friends to also use Torrust. One option, when public facing registrations have closed, is that uses are granted a certain amount of 'invitation slots' that their friends may use.

Users who invite other users should be linked, so that the Torrust Administrators can easily resolve issues when invitations have unfortunately brought unfriendly people into the community.

Power-Users may want to setup systems that allow them to automatically generate new invitations. It is convenient for these systems to run independently without need of a connection to the Torrust-Backend API.

The Torrust-Backend should not store any secrets from the users. Just like the use of appropriately implemented hashed passwords, likewise invitations should not rely on secrets held by the backend.

Backend Database

Invitations

TABLE USER . INVITATIONS
KEY SIGNED_DIGEST The 32-byte message hash that was signed.
VALUES:

  1. PUBLIC_KEY LINK USER.INVITATION_PUBLIC_KEYS.PUBLIC_KEY
  2. INVITATION_BEGIN
  3. INVITATION_EXPIRY
  4. COUNT_USES
  5. COUNT_PENDING_USES When an active attempt for registration is in process. When an attempt fails or times-out, this number is reduced.
  6. COUNT_MAX_USES Total Max. Users + Pending.
  7. PERSONAL_MESSAGE of this invitation.

Invitation Slots

TABLE USER . INVITATION_SLOTS
KEY USER User that was successfully registered using this invitation slot.
VALUES:

  1. INVITATION LINK USER.INVITATIONS.SIGNED_DIGEST
  2. DATE_USED
  3. USE_COUNT at time of registration, how many times this invitation had been used.

User Public Keys

TABLE USER . INVITATION_PUBLIC_KEYS
KEY PUBLIC_KEY The 32-byte Public Key.
VALUES:

  1. DATE_REGISTERED
  2. DATE_BEGIN Date when this public key begins to be active. I.e. Status WAITING
  3. DATE_EXPIRY
  4. COUNT_USES
  5. COUNT_PENDING_USES The number of ongoing registration attempts now. i.e. How many people who have used valid invitations to begin the registration process, but haven't activated their accounts yet, such as still waiting for their email to confirm, etc. When an attempt fails or times-out, this number is reduced.
  6. COUNT_PENDING_USES_MAX The Maximum Number of simultaneously registering attempts.
  7. COUNT_MAX_USES Total Max. Users + Pending.
  8. ONGOING_REGISTRATION_COUNT
  9. ONGOING_REGISTRATION_COUNT_MAX
  10. STATUS ENUM WAITING. ACTIVE. EXPIRED. MAX_REACHED. REVOKED.

User

TABLE USER
NEW_VALUES:

  1. INVITATIONS_MAX The maximum number of invitations.
  2. INVITATIONS TABLE
  3. INVITATION_SLOTS_MAX The maximum number of invitation slots.
  4. INVITATION_SLOTS TABLE
  5. INVITATION_PUBLIC_KEYS_MAX The maximum number of Public Keys.
  6. INVITATION_PUBLIC_KEYS_MAX_ACTIVE The maximum number of Public Keys in Active State.
  7. INVITATION_PUBLIC_KEYS_MAX_WAITING The maximum number of Public Keys in Waiting State.
  8. INVITATION_PUBLIC_KEYS TABLE
  9. INVITED_BY LINK.OPTIONAL USER (Optional. Some users are not invited.)

Invitation Format

Datatypes:

PrivateKey.
32-byte array. Uniform Random. Generated by User.

PublicKey.
32-byte array. Derived from PrivateKey.

Signature.
64-byte array. Created by signing the invitation.

Invitation_Digest.
32-byte array. Created by the tagged_hash of the signing algorithm from the Invitation_Payload.

UserID.
8-byte array. Unique. Received from Server.

Begin.
8-byte array. 64-bit Unix Time. UTC.

Expiry.
8-byte array. 64-bit Unix Time. UTC.

Uses_Max.
4-byte array. Zero for Unlimited.

Personal_Message.
Variable-Length. 0 to Max 480-bytes. UTC-8 Encoded.
**NOTE. This field MUST be excepted properly when displayed, and stored within the JSON, and the Database.**

Invitation_Payload.
Variable-Length. Min. 8 + 8 + 8 + 4 + 0 = 20-bytes. Max. 20 + 480 = 500-bytes.
UserID || Begin || Expiry || Uses_Max || Personal_Message

Invitation.
Variable-Length. Min. 64 + 20 = 84-bytes. Max. 64 + 500 = 564-bytes.
Signature || Invitation_Payload

Encoded Formats.

Encoding using Bech32m

Encoded PrivateKey.
human-readable part: torrust_bip340_pk
separator: 1
data part: PrivateKey (32-byte)


Note, this is used for the backup-transfer of the private key used to generate new invitations.


Invitation.
human-readable part: UNIQUE_TO_NODE
separator: 1
data part: Invitation. (Variable-Length. 84-byte to 564-bytes)

The invitation is encoded and decoded by the frontend.

@da2ce7
Copy link
Contributor

da2ce7 commented May 29, 2022

@WarmBeer @BelieveInBunny @george-avn Please Review. 👍

@mickvandijke
Copy link
Member

Hey @da2ce7 ,

Thank you for the very interesting and detailed proposal. I am however a little bit ignorant of the use case of such a complicated user invitation system. Could you explain to me the benefit of generating these invitation tokens independently from a Torrust Index backend so I understand why this is needed?

@da2ce7
Copy link
Contributor

da2ce7 commented Jun 7, 2022

Hey @WarmBeer,

Sure, There are in-fact two competing proposals:

Invitation Codes (Interactive Code Generation): torrust/torrust-index#30 (comment)

Signed Invitation Codes (Non-interactive Code Generation): torrust/torrust-index#30 (comment)


The main advantage of the non-interactive code generation in the case that this software becomes very popular.

  1. It allows websites to make 'invitation portals' without needing to connect to the torrust-index api.
  2. It will also enable the possibility that users to generate invitations 'offline', maybe at a conference or something.

Overall I think that it is a cool thing to have, so "coolness factor" may be a thing. When you have public keys connected to an account, we could extend the same system to work as account-recovery or two-factor, or even implement other privacy features.

:)

Cam.

@mickvandijke
Copy link
Member

Hey @da2ce7 ,

I made an activity diagram of what happens when the backend receives a user registration request.

We talked about how to store pending user registrations in the database and how to clean them up. The ideas we discussed were:

1. Create a separate DB table "pending_users" and store new users in that table until they are verified (on which they will move to the "user" table). The table will also have a scheduler to automatically expire rows that didn't verify in time.

Pros:

  • The "user" table only contains verified users.

Cons:

  • Requires a "smart" database. If we keep using the database as purely storage without any logic, it will be easier to implement other database solutions.

2. Keep using the "emailed_verified" flag in the "users" table. The backend will then have to run a cronjob to remove users that took too long to verify their email.

Pros:

  • We don't have to make changes to the current "user" table.

Cons:

  • The "user" table will be contaminated with unverified accounts.
  • Cronjobs always run at a certain interval and that will result into variable timeframes of being able to verify user accounts.

While working on the activity diagram, I thought of another solution:

3. Store unverified accounts in memory (with something like Redis) and only submit them to the "user" database on verification.

Pros:

  • We keep all the logic of verifying accounts in the backend, which gives us more flexibility if we want to make changes to that process.
  • Less querying of the database compared to the other two solutions.

Cons:

  • More memory usage (~80 bytes per pending user)

Let me know what you think :)

torrust-user-invite-signup drawio

@da2ce7
Copy link
Contributor

da2ce7 commented Sep 20, 2023

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants