Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public ResponseEntity<?> createKey(@RequestBody KeyDTO dto, Principal principal)
var username = principal.getName();

try {
var created = keyService.createKey(dto.getAlias(), dto.getKeyType(), dto.getAllowedOperations(), username);
var created = keyService.createKey(dto.getAlias(), dto.getKeyType(), username);
return ResponseEntity.status(HttpStatus.CREATED).body(mapper.toDto(created));
} catch (InvalidParameterException e) {
return ResponseEntity.badRequest().body(e.getMessage());
Expand Down
3 changes: 0 additions & 3 deletions MiniKms/src/main/java/ftn/security/minikms/dto/KeyDTO.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package ftn.security.minikms.dto;

import ftn.security.minikms.enumeration.KeyOperation;
import ftn.security.minikms.enumeration.KeyType;
import lombok.*;

import java.util.List;
import java.util.UUID;

@Data
Expand All @@ -13,5 +11,4 @@ public class KeyDTO {
private UUID id;
private String alias;
private KeyType keyType;
private List<KeyOperation> allowedOperations;
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,14 @@ KeyType.HMAC, new HMACService()
);
}

public KeyMetadata createKey(String alias, KeyType keyType, List<KeyOperation> allowedOperations, String username)
public KeyMetadata createKey(String alias, KeyType keyType, String username)
throws InvalidParameterException, GeneralSecurityException {
var user = findUserByUsername(username);
var allowedOperations = switch (keyType) {
case SYMMETRIC -> List.of(KeyOperation.ENCRYPT);
case ASYMMETRIC -> List.of(KeyOperation.ENCRYPT, KeyOperation.SIGN);
case HMAC -> List.of(KeyOperation.SIGN);
};
var metadata = metadataRepository.save(KeyMetadata.of(alias, keyType, allowedOperations, user));
return createNewKeyVersion(metadata, 1);
}
Expand Down
4 changes: 2 additions & 2 deletions MiniKms/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ server.ssl.key-alias=minikms

# Debugging
#spring.jpa.show-sql=true
#logging.level.org.springframework.security=DEBUG
#logging.level.io.jsonwebtoken=DEBUG
logging.level.org.springframework.security=DEBUG
logging.level.io.jsonwebtoken=DEBUG

# 1 hour
jwt.expiration=3600000
Expand Down
26 changes: 26 additions & 0 deletions MiniKmsClient/src/app/api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ export interface LoginResponse {
token: string;
}

export interface KeyMetadata {
keyId: string;
alias: string;
primaryVersion: number;
keyType: string;
allowedOperations?: string[];
createdAt?: Date;
rotatedAt?: Date;
}

@Injectable({
providedIn: 'root'
})
Expand All @@ -23,4 +33,20 @@ export class ApiService {
login(credentials: LoginRequest): Observable<LoginResponse> {
return this.http.post<LoginResponse>(`${this.baseUrl}/auth`, credentials);
}

getKeys(): Observable<KeyMetadata[]> {
return this.http.get<KeyMetadata[]>(`${this.baseUrl}/keys`);
}

createKey(alias: string, keyType: string): Observable<KeyMetadata> {
return this.http.post<KeyMetadata>(`${this.baseUrl}/keys/create`, { alias, keyType });
}

rotateKey(id: string): Observable<KeyMetadata> {
return this.http.post<KeyMetadata>(`${this.baseUrl}/keys/rotate`, { id });
}

deleteKey(id: string): Observable<void> {
return this.http.delete<void>(`${this.baseUrl}/keys/${id}`);
}
}
17 changes: 17 additions & 0 deletions MiniKmsClient/src/app/app.component.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
.app-wrapper {
height: 100%;
display: flex;
flex-direction: column;

mat-toolbar {
position: sticky;
top: 0;
z-index: 1000;
flex-shrink: 0;
}
}

.content {
flex: 1 0 auto;
}

.spacer {
flex: 1 1 auto;
}
Expand Down
31 changes: 16 additions & 15 deletions MiniKmsClient/src/app/app.component.html
Original file line number Diff line number Diff line change
@@ -1,35 +1,36 @@
<div class="app-wrapper">

<mat-toolbar color="primary">
<span>Mini KMS Client</span>

<span class="spacer"></span>

<nav *ngIf="isAuthenticated()">
<!-- Manager can see both buttons -->
<a mat-button
routerLink="/manage-keys"
routerLinkActive="active-link"
*ngIf="isManager()">
Manage Keys
<a mat-button routerLink="/manage-keys" routerLinkActive="active-link" *ngIf="isManager()">
Manage Keys
</a>

<!-- Both manager and user can see crypto -->
<a mat-button
routerLink="/crypto"
routerLinkActive="active-link">
Crypto
<a mat-button routerLink="/crypto" routerLinkActive="active-link">
Crypto
</a>

<!-- Logout button for authenticated users -->
<button mat-button (click)="logout()">
<mat-icon>logout</mat-icon>
Logout
</button>
</nav>

<!-- Login button for non-authenticated users -->
<nav *ngIf="!isAuthenticated()">
<a mat-button routerLink="/login" routerLinkActive="active-link">Login</a>
</nav>
</mat-toolbar>

<router-outlet></router-outlet>
<div class="content">
<router-outlet></router-outlet>
</div>

</div>
11 changes: 9 additions & 2 deletions MiniKmsClient/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ReactiveFormsModule, FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { LoginComponent } from './login/login.component';
import { ManageKeysComponent } from './manage-keys/manage-keys.component';
import { CryptoComponent } from './crypto/crypto.component';
import { MaterialModule } from './common/material/material.module';
import { AuthInterceptorService } from './auth/auth.interceptor';

@NgModule({
declarations: [
Expand All @@ -27,7 +28,13 @@ import { MaterialModule } from './common/material/material.module';
AppRoutingModule,
MaterialModule
],
providers: [],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptorService,
multi: true
}
],
bootstrap: [AppComponent]
})
export class AppModule { }
17 changes: 17 additions & 0 deletions MiniKmsClient/src/app/auth/auth.interceptor.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { TestBed } from '@angular/core/testing';
import { HttpInterceptorFn } from '@angular/common/http';

import { authInterceptor } from './auth.interceptor';

describe('authInterceptor', () => {
const interceptor: HttpInterceptorFn = (req, next) =>
TestBed.runInInjectionContext(() => authInterceptor(req, next));

beforeEach(() => {
TestBed.configureTestingModule({});
});

it('should be created', () => {
expect(interceptor).toBeTruthy();
});
});
35 changes: 35 additions & 0 deletions MiniKmsClient/src/app/auth/auth.interceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { HttpInterceptorFn, HttpErrorResponse, HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { catchError, Observable, throwError } from 'rxjs';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
const router = inject(Router);
const token = localStorage.getItem('token');

let authReq = req;
if (token) {
authReq = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
}

return next(authReq).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401 || error.status === 403) {
localStorage.removeItem('token');
router.navigate(['/login']);
}
return throwError(() => error);
})
);
};

@Injectable()
export class AuthInterceptorService implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return authInterceptor(req, next.handle.bind(next));
}
}
4 changes: 3 additions & 1 deletion MiniKmsClient/src/app/login/login.component.scss
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
.login-container {
min-height: 100vh;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
padding: 24px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
Expand All @@ -14,6 +15,7 @@
width: 100%;
max-width: 420px;
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.15);
margin-bottom: 100px;
}

.login-header {
Expand Down
7 changes: 6 additions & 1 deletion MiniKmsClient/src/app/login/login.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,12 @@ export class LoginComponent implements OnInit {
},
error: (error) => {
console.error('Login error:', error);
this.snackBar.open('Invalid credentials', 'Close', { duration: 3000 });
if (error.status === 0) {
this.snackBar.open('Server is unreachable', 'Close', { duration: 3000 });
} else if (error.status === 401 || error.status === 403) {
this.snackBar.open('Invalid credentials', 'Close', { duration: 3000 });
}

this.setLoadingState(false);
}
});
Expand Down
57 changes: 47 additions & 10 deletions MiniKmsClient/src/app/manage-keys/manage-keys.component.html
Original file line number Diff line number Diff line change
@@ -1,29 +1,34 @@
<div class="controls-row">
<mat-form-field appearance="outline" class="control">
<mat-label>Alias</mat-label>
<input matInput placeholder="Enter alias">
<input matInput placeholder="Enter alias" [(ngModel)]="alias" [disabled]="isCreating">
</mat-form-field>

<mat-form-field appearance="outline" class="control">
<mat-label>Key Type</mat-label>
<mat-select>
<mat-select [(ngModel)]="selectedKeyType" [disabled]="isCreating">
<mat-option *ngFor="let type of keyTypes" [value]="type">
{{ type }}
</mat-option>
</mat-select>
</mat-form-field>

<button mat-raised-button color="primary" class="add-btn" (click)="addKey()">
Add Key
<button mat-raised-button color="primary" class="add-btn" (click)="addKey()" [disabled]="isCreating">
<span *ngIf="!isCreating">Add Key</span>
</button>

<!-- Loading spinner next to button -->
<div *ngIf="isLoading || isCreating" class="loading-indicator">
<mat-spinner diameter="30"></mat-spinner>
</div>
</div>

<table mat-table [dataSource]="dataSource" class="mat-elevation-z2 full-width-table">

<!-- Key ID -->
<ng-container matColumnDef="keyId">
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef> Key ID </th>
<td mat-cell *matCellDef="let element"> {{element.keyId}} </td>
<td mat-cell *matCellDef="let element"> {{element.id}} </td>
</ng-container>

<!-- Alias -->
Expand All @@ -38,19 +43,51 @@
<td mat-cell *matCellDef="let element"> {{element.keyType}} </td>
</ng-container>

<!-- Current version -->
<ng-container matColumnDef="currentVersion">
<th mat-header-cell *matHeaderCellDef> Current Version </th>
<td mat-cell *matCellDef="let element"> {{element.primaryVersion}} </td>
</ng-container>

<!-- Allowed Operations -->
<ng-container matColumnDef="allowedOperations">
<th mat-header-cell *matHeaderCellDef> Allowed Operations </th>
<td mat-cell *matCellDef="let element"> {{element.allowedOperations?.join(', ')}} </td>
</ng-container>

<!-- Created at -->
<ng-container matColumnDef="createdAt">
<th mat-header-cell *matHeaderCellDef> Created At </th>
<td mat-cell *matCellDef="let element"> {{element.createdAt | date:'short'}} </td>
</ng-container>

<!-- Rotated at -->
<ng-container matColumnDef="rotatedAt">
<th mat-header-cell *matHeaderCellDef> Rotated At </th>
<td mat-cell *matCellDef="let element"> {{element.rotatedAt ? (element.rotatedAt | date:'short') : 'Never' }} </td>
</ng-container>

<!-- Actions -->
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> Actions </th>
<td mat-cell *matCellDef="let element" class="actions-cell">
<button mat-stroked-button color="warn" (click)="deleteKey(element)">
Delete
<button mat-icon-button color="primary" (click)="rotateKey(element.id)" matTooltip="Rotate Key">
<mat-icon>refresh</mat-icon>
</button>
<button mat-stroked-button color="accent" (click)="rotateKey(element)">
Rotate
<button mat-icon-button color="warn" (click)="deleteKey(element.id)" matTooltip="Delete Key">
<mat-icon>delete</mat-icon>
</button>
</td>
</ng-container>

<!-- No data row -->
<ng-container matColumnDef="noData">
<td mat-cell *matCellDef colspan="8" class="no-data-cell">
No keys available
</td>
</ng-container>

<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
<tr mat-row *matRowDef="let row; columns: ['noData']; when: isNoData" class="no-data-row"></tr>
</table>
Loading
Loading