mirror of
https://github.com/Drop-OSS/drop.git
synced 2025-11-13 00:02:37 +10:00
handshakes
This commit is contained in:
@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- The primary key for the `Client` table will be changed. If it partially fails, the table could be left without primary key constraint.
|
||||||
|
- You are about to drop the column `sharedToken` on the `Client` table. All the data in the column will be lost.
|
||||||
|
- The required column `id` was added to the `Client` table with a prisma-level default value. This is not possible if the table is not empty. Please add this column as optional, then populate it before making it required.
|
||||||
|
- Added the required column `lastConnected` to the `Client` table without a default value. This is not possible if the table is not empty.
|
||||||
|
- Added the required column `name` to the `Client` table without a default value. This is not possible if the table is not empty.
|
||||||
|
- Added the required column `platform` to the `Client` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Client" DROP CONSTRAINT "Client_pkey",
|
||||||
|
DROP COLUMN "sharedToken",
|
||||||
|
ADD COLUMN "id" TEXT NOT NULL,
|
||||||
|
ADD COLUMN "lastConnected" TIMESTAMP(3) NOT NULL,
|
||||||
|
ADD COLUMN "name" TEXT NOT NULL,
|
||||||
|
ADD COLUMN "platform" TEXT NOT NULL,
|
||||||
|
ADD CONSTRAINT "Client_pkey" PRIMARY KEY ("id");
|
||||||
@ -43,15 +43,16 @@ enum ClientCapabilities {
|
|||||||
|
|
||||||
// References a device
|
// References a device
|
||||||
model Client {
|
model Client {
|
||||||
sharedToken String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
userId String
|
userId String
|
||||||
user User @relation(fields: [userId], references: [id])
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
|
||||||
endpoint String
|
endpoint String
|
||||||
capabilities ClientCapabilities[]
|
capabilities ClientCapabilities[]
|
||||||
|
|
||||||
name String
|
name String
|
||||||
platform String
|
platform String
|
||||||
|
lastConnected DateTime
|
||||||
}
|
}
|
||||||
|
|
||||||
enum MetadataSource {
|
enum MetadataSource {
|
||||||
|
|||||||
@ -12,7 +12,7 @@ export default defineEventHandler(async (h3) => {
|
|||||||
statusMessage: "Provide client ID in request params as 'id'",
|
statusMessage: "Provide client ID in request params as 'id'",
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await clientHandler.fetchInitiateClientMetadata(
|
const data = await clientHandler.fetchClientMetadata(
|
||||||
providedClientId
|
providedClientId
|
||||||
);
|
);
|
||||||
if (!data)
|
if (!data)
|
||||||
|
|||||||
@ -7,7 +7,7 @@ export default defineEventHandler(async (h3) => {
|
|||||||
const body = await readBody(h3);
|
const body = await readBody(h3);
|
||||||
const clientId = await body.id;
|
const clientId = await body.id;
|
||||||
|
|
||||||
const data = await clientHandler.fetchInitiateClientMetadata(clientId);
|
const data = await clientHandler.fetchClientMetadata(clientId);
|
||||||
if (!data)
|
if (!data)
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
|
|||||||
@ -1,3 +1,46 @@
|
|||||||
export default defineEventHandler((h3) => {
|
import clientHandler from "~/server/internal/clients/handler";
|
||||||
|
import { useGlobalCertificateAuthority } from "~/server/plugins/ca";
|
||||||
|
|
||||||
})
|
export default defineEventHandler(async (h3) => {
|
||||||
|
const body = await readBody(h3);
|
||||||
|
const clientId = body.clientId;
|
||||||
|
const token = body.token;
|
||||||
|
if (!clientId || !token)
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: "Missing token or client ID from body",
|
||||||
|
});
|
||||||
|
|
||||||
|
const metadata = await clientHandler.fetchClient(clientId);
|
||||||
|
if (!metadata)
|
||||||
|
throw createError({
|
||||||
|
statusCode: 403,
|
||||||
|
statusMessage: "Invalid client ID",
|
||||||
|
});
|
||||||
|
if (!metadata.authToken || !metadata.userId)
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: "Un-authorized client ID",
|
||||||
|
});
|
||||||
|
if (metadata.authToken !== token)
|
||||||
|
throw createError({
|
||||||
|
statusCode: 403,
|
||||||
|
statusMessage: "Invalid token",
|
||||||
|
});
|
||||||
|
|
||||||
|
const ca = useGlobalCertificateAuthority();
|
||||||
|
const bundle = await ca.generateClientCertificate(
|
||||||
|
clientId,
|
||||||
|
metadata.data.name
|
||||||
|
);
|
||||||
|
|
||||||
|
const client = await clientHandler.finialiseClient(clientId);
|
||||||
|
await ca.storeClientCertificate(clientId, bundle);
|
||||||
|
|
||||||
|
return {
|
||||||
|
private: bundle.priv,
|
||||||
|
public: bundle.pub,
|
||||||
|
certificate: bundle.cert,
|
||||||
|
id: client.id,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|||||||
@ -17,7 +17,7 @@ Client makes request: `POST /api/v1/client/handshake` with the token recieved in
|
|||||||
|
|
||||||
The server uses it's CA to generate a public-private key pair, the CN of the client ID. It then sends that pair, plus the CA's public key, to the client, which stores it all.
|
The server uses it's CA to generate a public-private key pair, the CN of the client ID. It then sends that pair, plus the CA's public key, to the client, which stores it all.
|
||||||
|
|
||||||
The certificate lasts for a year, and is rotated when it has 3 months or less left on it's expiry.
|
*The certificate lasts for a year, and is rotated when it has 3 months or less left on it's expiry.*
|
||||||
|
|
||||||
## 4.a Client requests one-time device endpoint
|
## 4.a Client requests one-time device endpoint
|
||||||
The client generates a nonce and signs it with their private key. This is then attached to any device-related request.
|
The client generates a nonce and signs it with their private key. This is then attached to any device-related request.
|
||||||
|
|||||||
@ -31,4 +31,26 @@ export class CertificateAuthority {
|
|||||||
}
|
}
|
||||||
return new CertificateAuthority(store, root);
|
return new CertificateAuthority(store, root);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async generateClientCertificate(clientId: string, clientName: string) {
|
||||||
|
const caCertificate = await this.certificateStore.fetch("ca");
|
||||||
|
if (!caCertificate)
|
||||||
|
throw new Error("Certificate authority not initialised");
|
||||||
|
const [priv, pub, cert] = droplet.generateClientCertificate(
|
||||||
|
clientId,
|
||||||
|
clientName,
|
||||||
|
caCertificate.cert,
|
||||||
|
caCertificate.priv
|
||||||
|
);
|
||||||
|
const certBundle: CertificateBundle = {
|
||||||
|
priv,
|
||||||
|
pub,
|
||||||
|
cert,
|
||||||
|
};
|
||||||
|
return certBundle;
|
||||||
|
}
|
||||||
|
|
||||||
|
async storeClientCertificate(clientId: string, bundle: CertificateBundle) {
|
||||||
|
await this.certificateStore.store(`client:${clientId}`, bundle);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import { CertificateBundle } from "./ca";
|
||||||
|
import prisma from "../db/database";
|
||||||
|
|
||||||
export interface ClientMetadata {
|
export interface ClientMetadata {
|
||||||
name: string;
|
name: string;
|
||||||
@ -29,10 +31,14 @@ export class ClientHandler {
|
|||||||
return clientId;
|
return clientId;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchInitiateClientMetadata(clientId: string) {
|
async fetchClientMetadata(clientId: string) {
|
||||||
|
return (await this.fetchClient(clientId))?.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchClient(clientId: string) {
|
||||||
const entry = this.temporaryClientTable[clientId];
|
const entry = this.temporaryClientTable[clientId];
|
||||||
if (!entry) return undefined;
|
if (!entry) return undefined;
|
||||||
return entry.data;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
async attachUserId(clientId: string, userId: string) {
|
async attachUserId(clientId: string, userId: string) {
|
||||||
@ -50,6 +56,32 @@ export class ClientHandler {
|
|||||||
|
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fetchClientMetadataByToken(token: string) {
|
||||||
|
return Object.entries(this.temporaryClientTable)
|
||||||
|
.map((e) => Object.assign(e[1], { id: e[0] }))
|
||||||
|
.find((e) => e.authToken === token);
|
||||||
|
}
|
||||||
|
|
||||||
|
async finialiseClient(id: string) {
|
||||||
|
const metadata = this.temporaryClientTable[id];
|
||||||
|
if (!metadata) throw new Error("Invalid client ID");
|
||||||
|
if (!metadata.userId) throw new Error("Un-authorized client ID");
|
||||||
|
|
||||||
|
return await prisma.client.create({
|
||||||
|
data: {
|
||||||
|
id: id,
|
||||||
|
userId: metadata.userId,
|
||||||
|
|
||||||
|
endpoint: "",
|
||||||
|
capabilities: [],
|
||||||
|
|
||||||
|
name: metadata.data.name,
|
||||||
|
platform: metadata.data.platform,
|
||||||
|
lastConnected: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const clientHandler = new ClientHandler();
|
export const clientHandler = new ClientHandler();
|
||||||
|
|||||||
Reference in New Issue
Block a user