Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cats #5

Merged
merged 16 commits into from
Mar 4, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 29 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,29 +25,39 @@ Assumptions:

## VSS bootstrap architecture example

What examplary problem VSS bootstrap solves?
The goal of the system is to check password security and return the information if the given password (or password hash) is safe to be used. For each check request we will try to crack the password or check it against common known leaks.
The system can be fed with a stream of plain text password combinations that should be forbidden to use (treated as easy to crack).
What exemplary problem VSS bootstrap solves?
The goal of the system is to get password hashed or check whether a given password hash has been queried before.

![VSS bootstrap architecture](docs/architecture.drawio.svg)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] the drawing is very hard to read with dark mode :)



## Technology and functionalities

Check Password Service:
Base Service:
* HTTP/gRPC endpoints
* Storing forbidden combinations in database
* Caching layer for recently cracked/used password
* Providing password hashes for the given password hash type pair
* Checking if a given password hash has been queried in the hash service before
* Pub/Sub solution for sending events about checked passwords

* Tracing information about performed operations

Stats Service:
* Read check results events from Pub/Sub
* Displaying current system stats and browsing the historical checks

* Displaying all the accumulated events by HTTP/gRPC

## Solution

### Setting up the local environment

Docker compose
```
docker-compose up
```

Pulumi
```
TODO
```

### Run VSS demo app:

VSS Vanilla
Expand All @@ -60,6 +70,13 @@ VSS ZIO
$ sbt "vss_zio/runMain com.virtuslab.vss.zio.MainZIO"
```

### VSS Cats
KacperFKorban marked this conversation as resolved.
Show resolved Hide resolved

VSS Cats
```
sbt "vss_cats/run"
```

### Use it:

HTTP docs:
Expand All @@ -74,14 +91,13 @@ curl -X 'POST' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"hashType": "MD5",
"hashType": "SHA256",
"password": "some_password"
}'
```

gRPC request example:
gRPC request example (for vss cats):

```bash
grpcurl -d '{"hashType": "MD5", "password": "somepassword"}' --import-path vss-vanilla/src/main/protobuf --proto password.proto --plaintext localhost:8181 com.virtuslab.vss.proto.HashPasswordService/HashPassword
grpcurl -d '{"hashType": "SHA256", "password": "somepassword"}' --import-path vss-cats/src/main/protobuf --proto password.proto --plaintext localhost:8081 com.virtuslab.vss.proto.cats.HashPasswordService/HashPassword
KacperFKorban marked this conversation as resolved.
Show resolved Hide resolved
```

45 changes: 43 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
ThisBuild / version := "0.1.0-SNAPSHOT"

ThisBuild / scalaVersion := "3.2.2"
ThisBuild / scalaVersion := "3.3.0"

val tapirVersion = "1.2.10"

lazy val root = (project in file("."))
.settings(
name := "vss-bootstrap"
)
.aggregate(vss_vanilla, vss_zio, commons)
.aggregate(vss_vanilla, vss_zio, vss_cats, commons)

val commonSettings = (
scalacOptions ++= Seq(
"-Ykind-projector"
)
)

lazy val commons = (project in file("commons"))
.settings(commonSettings)
.settings(
libraryDependencies ++= Seq(
"ch.qos.logback" % "logback-classic" % "1.4.5",
Expand All @@ -24,6 +31,7 @@ lazy val commons = (project in file("commons"))
)

lazy val vss_vanilla = (project in file("vss-vanilla"))
.settings(commonSettings)
.settings(
Compile / PB.targets := Seq(
scalapb.gen() -> (Compile / sourceManaged).value / "scalapb"
Expand All @@ -37,6 +45,7 @@ lazy val vss_vanilla = (project in file("vss-vanilla"))
.dependsOn(commons)

lazy val vss_zio = (project in file("vss-zio"))
.settings(commonSettings)
.settings(
Compile / PB.targets := Seq(
scalapb.gen(grpc = true) -> (Compile / sourceManaged).value,
Expand All @@ -48,3 +57,35 @@ lazy val vss_zio = (project in file("vss-zio"))
)
)
.dependsOn(commons)

val http4sVersion = "0.23.18"
val fs2Version = "3.6.1"
val catsEffectVersion = "3.4.8"
val doobieVersion = "1.0.0-RC2"
KacperFKorban marked this conversation as resolved.
Show resolved Hide resolved
val monocleVersion = "3.2.0"

lazy val vss_cats = project.in(file("vss-cats"))
.settings(commonSettings)
.settings(
libraryDependencies ++= Seq(
"org.typelevel" %% "cats-effect" % catsEffectVersion,
"co.fs2" %% "fs2-core" % fs2Version,
"com.github.fd4s" %% "fs2-kafka" % "3.0.0-RC1",
KacperFKorban marked this conversation as resolved.
Show resolved Hide resolved
"org.http4s" %% "http4s-server" % http4sVersion,
"org.http4s" %% "http4s-ember-server" % http4sVersion,
"org.http4s" %% "http4s-circe" % http4sVersion,
"org.http4s" %% "http4s-dsl" % http4sVersion,
"org.tpolecat" %% "doobie-core" % doobieVersion,
"org.tpolecat" %% "doobie-postgres" % doobieVersion,
"org.tpolecat" %% "doobie-hikari" % doobieVersion,
"dev.optics" %% "monocle-core" % monocleVersion,
"dev.optics" %% "monocle-macro" % monocleVersion,
"is.cir" %% "ciris" % "3.1.0",
"com.softwaremill.sttp.tapir" %% "tapir-http4s-server" % tapirVersion,
"org.tpolecat" %% "natchez-http4s" % "0.5.0",
"org.tpolecat" %% "natchez-jaeger" % "0.3.0",
"io.grpc" % "grpc-netty-shaded" % scalapb.compiler.Version.grpcJavaVersion
)
)
.enablePlugins(Fs2Grpc)
.dependsOn(commons)
7 changes: 7 additions & 0 deletions commons/src/main/resources/tables.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
create table hashed_passwords(
KacperFKorban marked this conversation as resolved.
Show resolved Hide resolved
uuid UUID DEFAULT gen_random_uuid() PRIMARY KEY,
password VARCHAR NOT NULL,
KacperFKorban marked this conversation as resolved.
Show resolved Hide resolved
hash_type VARCHAR NOT NULL,
password_hash VARCHAR NOT NULL,
UNIQUE (hash_type, password)
KacperFKorban marked this conversation as resolved.
Show resolved Hide resolved
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.virtuslab.vss.common
KacperFKorban marked this conversation as resolved.
Show resolved Hide resolved

import sttp.tapir.*
import sttp.tapir.generic.auto.*
import sttp.tapir.json.upickle.*
import upickle.default.*

object BaseEndpoints:

val checkPasswordEndpoint: Endpoint[Unit, CheckPwned, Unit, CheckedPwned, Any] = sttp.tapir.endpoint.post
KacperFKorban marked this conversation as resolved.
Show resolved Hide resolved
.in("check")
.in(jsonBody[CheckPwned].example(CheckPwned("5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8")))
.out(jsonBody[CheckedPwned].example(CheckedPwned("5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8", true)))

val hashPasswordEndpoint: Endpoint[Unit, HashPassword, Unit, HashedPassword, Any] = sttp.tapir.endpoint.post
.in("hash")
.in(jsonBody[HashPassword].example(HashPassword("SHA256", "password")))
.out(
jsonBody[HashedPassword]
.example(HashedPassword("SHA256", "password", "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"))
)

case class CheckPwned(passwordHash: String)
object CheckPwned:
given ReadWriter[CheckPwned] = macroRW

case class CheckedPwned(passwordHash: String, pwned: Boolean)
object CheckedPwned:
given ReadWriter[CheckedPwned] = macroRW

case class HashPassword(hashType: String, password: String)
object HashPassword:
given ReadWriter[HashPassword] = macroRW

case class HashedPassword(hashType: String, password: String, hash: String)
object HashedPassword:
given ReadWriter[HashedPassword] = macroRW

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.virtuslab.vss.common

import sttp.tapir.*
import sttp.tapir.generic.auto.*
import sttp.tapir.json.upickle.*
import upickle.default.*

object StatsEndpoints:

val getAllEvents: Endpoint[Unit, Unit, Unit, List[Event], Any] = sttp.tapir.endpoint.get
.in("allevents")
.out(jsonBody[List[Event]].example(List(Event.CheckedPwned("5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"))))
14 changes: 14 additions & 0 deletions commons/src/main/scala/com/virtuslab/vss/common/events.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.virtuslab.vss.common

import upickle.default.*

enum Event:
case HashedPassword(password: String, hashType: String)
case CheckedPwned(passwordHash: String)

object Event {
given ReadWriter[Event] = ReadWriter.merge(
macroRW[Event.HashedPassword],
macroRW[Event.CheckedPwned]
)
}
53 changes: 53 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
version: '3.8'
services:
db:
restart: always
image: postgres:14.1-alpine
ports:
- "5432:5432"
environment:
- DEBUG=true
- POSTGRES_DB=vss
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
volumes:
- ./commons/src/main/resources/tables.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5

zookeeper:
image: confluentinc/cp-zookeeper:7.3.2
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000

kafka:
image: confluentinc/cp-kafka:7.3.2
ports:
- "9092:9092"
depends_on:
- zookeeper
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181'
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_INTERNAL:PLAINTEXT
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092,PLAINTEXT_INTERNAL://broker:29092
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1

jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "5775:5775/udp"
- "16686:16686" # Jaeger UI port
- "6831:6831/udp" # Jaeger Thrift Compact Protocol port
- "6832:6832/udp"
- "5778:5778"
- "14268:14268"
- "9411:9411"
environment:
- COLLECTOR_ZIPKIN_HTTP_PORT=9411
7 changes: 2 additions & 5 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6")

val zioGrpcVersion = "0.6.0-rc4"
libraryDependencies ++= Seq(
"com.thesamet.scalapb.zio-grpc" %% "zio-grpc-codegen" % zioGrpcVersion
)
libraryDependencies ++= Seq("com.thesamet.scalapb.zio-grpc" %% "zio-grpc-codegen" % "0.6.0-rc4")
addSbtPlugin("org.typelevel" % "sbt-fs2-grpc" % "2.5.11")
5 changes: 0 additions & 5 deletions src/main/scala/com/virtuslab/vss/main.scala

This file was deleted.

12 changes: 12 additions & 0 deletions vss-cats/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM hseeberger/scala-sbt

WORKDIR /..

# Copy your source code into the container
COPY . ..

# Compile your Scala application
RUN sbt compile

# Define the command to start your application
CMD ["sbt", "vss_cats/run"]
52 changes: 52 additions & 0 deletions vss-cats/src/main/protobuf/password.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
syntax = "proto3";
LukBed marked this conversation as resolved.
Show resolved Hide resolved

package com.virtuslab.vss.proto.cats;

// Base

message HashPasswordMessage {
string hashType = 1;
string password = 2;
}

message HashedPasswordMessage {
string hashType = 1;
string password = 2;
string hash = 3;
}

service HashPasswordService {
rpc HashPassword (HashPasswordMessage) returns (HashedPasswordMessage) {}
}

message CheckPwnedRequest {
string passwordHash = 1;
}

message CheckPwnedResponse {
string passwordHash = 1;
bool pwned = 2;
}

service PwnedService {
rpc CheckPwned (CheckPwnedRequest) returns (CheckPwnedResponse) {}
}

// Stats

message EmptyRequest {}

message EventResponse {
string eventType = 1;
optional string password = 2;
optional string hashType = 3;
optional string passwordHash = 4;
}

message AllEvents {
repeated EventResponse events = 1;
}

service StatsService {
rpc GetAllEvents (EmptyRequest) returns (AllEvents) {}
}
Loading