release: @marketplaces/auth 0.1.0 (built from main)
This commit is contained in:
114
dist/ed25519/ed25519-keypair.service.js
vendored
Normal file
114
dist/ed25519/ed25519-keypair.service.js
vendored
Normal file
@@ -0,0 +1,114 @@
|
||||
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
||||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
||||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
||||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
||||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
||||
};
|
||||
import { Injectable } from '@angular/core';
|
||||
/**
|
||||
* Manages the browser-local Ed25519 keypair used to sign admin auth
|
||||
* challenges. Real WebCrypto Ed25519 (RFC 8032 support landed in evergreen
|
||||
* browsers) - not a placeholder. The private key is generated
|
||||
* non-extractable and kept only in IndexedDB as a CryptoKey handle; it is
|
||||
* never serialized, never sent anywhere, and cannot be exported by design.
|
||||
*
|
||||
* Registering `publicKey` with an admin's account (associating it with a
|
||||
* role) is a backend-side, out-of-band operation (e.g. an Owner approving a
|
||||
* new admin's public key) - entirely outside this frontend's scope.
|
||||
*/
|
||||
const DB_NAME = 'admin-auth-ed25519';
|
||||
const DB_VERSION = 1;
|
||||
const STORE_NAME = 'keypair';
|
||||
const KEY_RECORD_ID = 'device-keypair';
|
||||
let Ed25519KeypairService = class Ed25519KeypairService {
|
||||
constructor() {
|
||||
this.cached = null;
|
||||
}
|
||||
isSupported() {
|
||||
return typeof crypto !== 'undefined' && !!crypto.subtle && typeof indexedDB !== 'undefined';
|
||||
}
|
||||
/** Returns the device's Ed25519 keypair, generating and persisting one on first use. */
|
||||
async getOrCreateKeyPair() {
|
||||
if (!this.isSupported()) {
|
||||
throw new Error('Ed25519 is not supported in this browser (requires WebCrypto + IndexedDB).');
|
||||
}
|
||||
const existing = await this.loadFromStore();
|
||||
if (existing) {
|
||||
this.cached = existing;
|
||||
return { publicKeyBase64: existing.publicKeyBase64 };
|
||||
}
|
||||
const generated = await this.generateAndPersist();
|
||||
this.cached = generated;
|
||||
return { publicKeyBase64: generated.publicKeyBase64 };
|
||||
}
|
||||
async sign(message) {
|
||||
const keyPair = this.cached ?? (await this.loadFromStore());
|
||||
if (!keyPair) {
|
||||
throw new Error('No Ed25519 keypair available - call getOrCreateKeyPair() first.');
|
||||
}
|
||||
const signatureBuffer = await crypto.subtle.sign('Ed25519', keyPair.privateKey, new TextEncoder().encode(message));
|
||||
return this.toBase64(new Uint8Array(signatureBuffer));
|
||||
}
|
||||
/** Discards the local keypair (e.g. "forget this device"). A new keypair on next login requires re-registration with the backend. */
|
||||
async clear() {
|
||||
this.cached = null;
|
||||
const db = await this.openDatabase();
|
||||
await new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(STORE_NAME, 'readwrite');
|
||||
tx.objectStore(STORE_NAME).delete(KEY_RECORD_ID);
|
||||
tx.oncomplete = () => resolve();
|
||||
tx.onerror = () => reject(tx.error);
|
||||
});
|
||||
}
|
||||
async generateAndPersist() {
|
||||
const keyPair = (await crypto.subtle.generateKey({ name: 'Ed25519' }, false, ['sign', 'verify']));
|
||||
const publicKeyRaw = await crypto.subtle.exportKey('raw', keyPair.publicKey);
|
||||
const publicKeyBase64 = this.toBase64(new Uint8Array(publicKeyRaw));
|
||||
const record = {
|
||||
id: KEY_RECORD_ID,
|
||||
publicKey: keyPair.publicKey,
|
||||
privateKey: keyPair.privateKey,
|
||||
publicKeyBase64
|
||||
};
|
||||
const db = await this.openDatabase();
|
||||
await new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(STORE_NAME, 'readwrite');
|
||||
tx.objectStore(STORE_NAME).put(record);
|
||||
tx.oncomplete = () => resolve();
|
||||
tx.onerror = () => reject(tx.error);
|
||||
});
|
||||
return record;
|
||||
}
|
||||
async loadFromStore() {
|
||||
const db = await this.openDatabase();
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(STORE_NAME, 'readonly');
|
||||
const request = tx.objectStore(STORE_NAME).get(KEY_RECORD_ID);
|
||||
request.onsuccess = () => resolve(request.result ?? null);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
openDatabase() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
request.onupgradeneeded = () => {
|
||||
if (!request.result.objectStoreNames.contains(STORE_NAME)) {
|
||||
request.result.createObjectStore(STORE_NAME, { keyPath: 'id' });
|
||||
}
|
||||
};
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
toBase64(bytes) {
|
||||
let binary = '';
|
||||
for (const byte of bytes) {
|
||||
binary += String.fromCharCode(byte);
|
||||
}
|
||||
return btoa(binary);
|
||||
}
|
||||
};
|
||||
Ed25519KeypairService = __decorate([
|
||||
Injectable({ providedIn: 'root' })
|
||||
], Ed25519KeypairService);
|
||||
export { Ed25519KeypairService };
|
||||
Reference in New Issue
Block a user