diff --git a/src/app/app.component.ts b/src/app/app.component.ts index cc5e0dcb1..876f1d4f1 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -10,7 +10,7 @@ import { CameraService } from './shared/camera/camera.service'; import { CaptureService } from './shared/capture/capture.service'; import { CollectorService } from './shared/collector/collector.service'; import { CapacitorFactsProvider } from './shared/collector/facts/capacitor-facts-provider/capacitor-facts-provider.service'; -import { WebCryptoApiSignatureProvider } from './shared/collector/signature/web-crypto-api-signature-provider/web-crypto-api-signature-provider.service'; +import { CaptureAppWebCryptoApiSignatureProvider } from './shared/collector/signature/capture-app-web-crypto-api-signature-provider/capture-app-web-crypto-api-signature-provider.service'; import { DiaBackendAssetUploadingService } from './shared/dia-backend/asset/uploading/dia-backend-asset-uploading.service'; import { DiaBackendAuthService } from './shared/dia-backend/auth/dia-backend-auth.service'; import { DiaBackendNotificationService } from './shared/dia-backend/notification/dia-backend-notification.service'; @@ -33,7 +33,7 @@ export class AppComponent { private readonly iconRegistry: MatIconRegistry, private readonly sanitizer: DomSanitizer, private readonly capacitorFactsProvider: CapacitorFactsProvider, - private readonly webCryptoApiSignatureProvider: WebCryptoApiSignatureProvider, + private readonly capAppWebCryptoApiSignatureProvider: CaptureAppWebCryptoApiSignatureProvider, private readonly captureService: CaptureService, private readonly cameraService: CameraService, private readonly errorService: ErrorService, @@ -92,10 +92,10 @@ export class AppComponent { } initializeCollector() { - this.webCryptoApiSignatureProvider.initialize(); + this.capAppWebCryptoApiSignatureProvider.initialize(); this.collectorService.addFactsProvider(this.capacitorFactsProvider); this.collectorService.addSignatureProvider( - this.webCryptoApiSignatureProvider + this.capAppWebCryptoApiSignatureProvider ); } diff --git a/src/app/features/home/custom-camera/custom-camera.page.ts b/src/app/features/home/custom-camera/custom-camera.page.ts index 9ffa95da9..3569a7742 100644 --- a/src/app/features/home/custom-camera/custom-camera.page.ts +++ b/src/app/features/home/custom-camera/custom-camera.page.ts @@ -5,6 +5,7 @@ import { OnInit, } from '@angular/core'; import { Router } from '@angular/router'; +import { CameraSource } from '@capacitor/camera'; import { Capacitor, PluginListenerHandle } from '@capacitor/core'; import { Platform } from '@ionic/angular'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; @@ -13,7 +14,7 @@ import { CustomOrientation, PreviewCamera, } from '@numbersprotocol/preview-camera'; -import { BehaviorSubject, combineLatest, interval, Subscription } from 'rxjs'; +import { BehaviorSubject, Subscription, combineLatest, interval } from 'rxjs'; import { finalize, map, @@ -49,6 +50,7 @@ export class CustomCameraPage implements OnInit, OnDestroy { maxRecordTimeInSeconds = MAX_RECORD_TIME_IN_MILLISECONDS / 1000; maxRecordTimeInMilliseconds = MAX_RECORD_TIME_IN_MILLISECONDS; curRecordTimeInPercent$ = new BehaviorSubject(0); + curCaptureCameraSource: CameraSource = CameraSource.Camera; isRecording$ = new BehaviorSubject(false); mode$ = new BehaviorSubject('photo'); @@ -161,14 +163,18 @@ export class CustomCameraPage implements OnInit, OnDestroy { // PreviewCamera Plugin methods private async onCapturePhotoFinished(data: CaptureResult): Promise { - this.uploadItem(data, 'image'); + this.prePublish(data, 'image', CameraSource.Camera); } private async onCaptureVideoFinished(data: CaptureResult): Promise { - this.uploadItem(data, 'video'); + this.prePublish(data, 'video', CameraSource.Camera); } - private async uploadItem(data: CaptureResult, type: 'image' | 'video') { + private async prePublish( + data: CaptureResult, + type: 'image' | 'video', + source: CameraSource + ) { if (data.errorMessage) { await this.errorService.toastError$(data.errorMessage).toPromise(); } else if (data.filePath) { @@ -181,6 +187,7 @@ export class CustomCameraPage implements OnInit, OnDestroy { this.curCaptureMimeType = mimeType; this.curCaptureType = type; this.curCaptureSrc = Capacitor.convertFileSrc(filePath); + this.curCaptureCameraSource = source; this.lastCaptureMode = this.mode$.value; this.mode$.next('pre-publish'); @@ -276,7 +283,8 @@ export class CustomCameraPage implements OnInit, OnDestroy { if (this.curCaptureFilePath && this.curCaptureType) { this.customCameraService.uploadToCapture( this.curCaptureFilePath, - this.curCaptureType + this.curCaptureType, + this.curCaptureCameraSource ); this.leaveCustomCamera(); } diff --git a/src/app/features/home/custom-camera/custom-camera.service.ts b/src/app/features/home/custom-camera/custom-camera.service.ts index 8a3161696..4ee1d7c38 100644 --- a/src/app/features/home/custom-camera/custom-camera.service.ts +++ b/src/app/features/home/custom-camera/custom-camera.service.ts @@ -1,6 +1,7 @@ import { HttpClient } from '@angular/common/http'; import { Inject, Injectable } from '@angular/core'; import { DomSanitizer } from '@angular/platform-browser'; +import { CameraSource } from '@capacitor/camera'; import { Capacitor } from '@capacitor/core'; import { FilesystemPlugin } from '@capacitor/filesystem'; import { Platform } from '@ionic/angular'; @@ -48,7 +49,11 @@ export class CustomCameraService { return newItem; } - async uploadToCapture(filePath: string, type: CustomCameraMediaType) { + async uploadToCapture( + filePath: string, + type: CustomCameraMediaType, + source: CameraSource + ) { const itemToUpload = this.mediaItemFromFilePath(filePath, type); try { @@ -57,7 +62,7 @@ export class CustomCameraService { .toPromise(); const base64 = await blobToBase64(itemBlob); const mimeType = itemToUpload.mimeType; - await this.captureService.capture({ base64, mimeType }); + await this.captureService.capture({ base64, mimeType, source }); await this.removeFile(filePath); } catch (error) { const errMsg = this.translocoService.translate(`error.internetError`); diff --git a/src/app/features/settings/go-pro/services/go-pro-media.service.ts b/src/app/features/settings/go-pro/services/go-pro-media.service.ts index 83a8de1c0..4f4fec6e9 100644 --- a/src/app/features/settings/go-pro/services/go-pro-media.service.ts +++ b/src/app/features/settings/go-pro/services/go-pro-media.service.ts @@ -3,6 +3,7 @@ import { Inject, Injectable } from '@angular/core'; import { DomSanitizer } from '@angular/platform-browser'; import '@capacitor-community/http'; import { Http } from '@capacitor-community/http'; +import { CameraSource } from '@capacitor/camera'; import { Capacitor } from '@capacitor/core'; import { Directory as FilesystemDirectory, @@ -84,7 +85,11 @@ export class GoProMediaService { const mimeType = urlIsImage(mediaFile.url) ? 'image/jpeg' : 'video/mp4'; isDownloaded = true; - await this.captureService.capture({ base64, mimeType }); + await this.captureService.capture({ + base64, + mimeType, + source: CameraSource.Camera, + }); isCaptured = true; // delete temp downloaded file diff --git a/src/app/features/settings/settings.page.ts b/src/app/features/settings/settings.page.ts index 07ea54409..1ad9ba90b 100644 --- a/src/app/features/settings/settings.page.ts +++ b/src/app/features/settings/settings.page.ts @@ -5,7 +5,7 @@ import { Clipboard } from '@capacitor/clipboard'; import { IonModal } from '@ionic/angular'; import { TranslocoService } from '@ngneat/transloco'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; -import { defer, EMPTY, Subject } from 'rxjs'; +import { EMPTY, Subject, defer } from 'rxjs'; import { catchError, concatMapTo, @@ -18,7 +18,7 @@ import { } from 'rxjs/operators'; import { BlockingActionService } from '../../shared/blocking-action/blocking-action.service'; import { CapacitorFactsProvider } from '../../shared/collector/facts/capacitor-facts-provider/capacitor-facts-provider.service'; -import { WebCryptoApiSignatureProvider } from '../../shared/collector/signature/web-crypto-api-signature-provider/web-crypto-api-signature-provider.service'; +import { CaptureAppWebCryptoApiSignatureProvider } from '../../shared/collector/signature/capture-app-web-crypto-api-signature-provider/capture-app-web-crypto-api-signature-provider.service'; import { ConfirmAlert } from '../../shared/confirm-alert/confirm-alert.service'; import { Database } from '../../shared/database/database.service'; import { DiaBackendAuthService } from '../../shared/dia-backend/auth/dia-backend-auth.service'; @@ -62,7 +62,8 @@ export class SettingsPage { private readonly requiredClicks = 7; showHiddenOption = false; - private readonly privateKey$ = this.webCryptoApiSignatureProvider.privateKey$; + private readonly privateKey$ = + this.capAppWebCryptoApiSignatureProvider.privateKey$; readonly privateKeyTruncated$ = this.privateKey$.pipe( map(key => { // eslint-disable-next-line @typescript-eslint/no-magic-numbers @@ -86,7 +87,7 @@ export class SettingsPage { private readonly versionService: VersionService, private readonly router: Router, private readonly route: ActivatedRoute, - private readonly webCryptoApiSignatureProvider: WebCryptoApiSignatureProvider, + private readonly capAppWebCryptoApiSignatureProvider: CaptureAppWebCryptoApiSignatureProvider, private readonly snackBar: MatSnackBar ) {} diff --git a/src/app/features/wallets/wallets.page.ts b/src/app/features/wallets/wallets.page.ts index cd290ee27..85dcf4066 100644 --- a/src/app/features/wallets/wallets.page.ts +++ b/src/app/features/wallets/wallets.page.ts @@ -8,7 +8,7 @@ import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { NgxQrcodeElementTypes } from '@techiediaries/ngx-qrcode'; import { BehaviorSubject, fromEvent } from 'rxjs'; import { concatMap, first, map, tap } from 'rxjs/operators'; -import { WebCryptoApiSignatureProvider } from '../../shared/collector/signature/web-crypto-api-signature-provider/web-crypto-api-signature-provider.service'; +import { CaptureAppWebCryptoApiSignatureProvider } from '../../shared/collector/signature/capture-app-web-crypto-api-signature-provider/capture-app-web-crypto-api-signature-provider.service'; import { DiaBackendAuthService } from '../../shared/dia-backend/auth/dia-backend-auth.service'; import { BUBBLE_IFRAME_URL } from '../../shared/dia-backend/secret'; import { DiaBackendWalletService } from '../../shared/dia-backend/wallet/dia-backend-wallet.service'; @@ -20,8 +20,8 @@ import { BubbleToIonicPostMessage } from '../../shared/iframe/iframe'; styleUrls: ['./wallets.page.scss'], }) export class WalletsPage { - readonly publicKey$ = this.webCryptoApiSignatureProvider.publicKey$; - readonly privateKey$ = this.webCryptoApiSignatureProvider.privateKey$; + readonly publicKey$ = this.capAppWebCryptoApiSignatureProvider.publicKey$; + readonly privateKey$ = this.capAppWebCryptoApiSignatureProvider.privateKey$; readonly assetWalletAddr$ = this.diaBackendWalletService.assetWalletAddr$; readonly networkConnected$ = this.diaBackendWalletService.networkConnected$; @@ -43,7 +43,7 @@ export class WalletsPage { private readonly diaBackendAuthService: DiaBackendAuthService, private readonly snackBar: MatSnackBar, private readonly translocoService: TranslocoService, - private readonly webCryptoApiSignatureProvider: WebCryptoApiSignatureProvider, + private readonly capAppWebCryptoApiSignatureProvider: CaptureAppWebCryptoApiSignatureProvider, private readonly router: Router, private readonly navController: NavController ) {} diff --git a/src/app/shared/actions/service/order-history.service.ts b/src/app/shared/actions/service/order-history.service.ts index 9aa3f8d48..81d9ba909 100644 --- a/src/app/shared/actions/service/order-history.service.ts +++ b/src/app/shared/actions/service/order-history.service.ts @@ -1,9 +1,9 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; -import { BehaviorSubject, combineLatest, defer, EMPTY, Observable } from 'rxjs'; +import { BehaviorSubject, EMPTY, Observable, combineLatest, defer } from 'rxjs'; import { concatMap, first, map, pluck, tap } from 'rxjs/operators'; -import { WebCryptoApiSignatureProvider } from '../../collector/signature/web-crypto-api-signature-provider/web-crypto-api-signature-provider.service'; +import { CaptureAppWebCryptoApiSignatureProvider } from '../../collector/signature/capture-app-web-crypto-api-signature-provider/capture-app-web-crypto-api-signature-provider.service'; import { DiaBackendAssetRepository } from '../../dia-backend/asset/dia-backend-asset-repository.service'; import { BUBBLE_DB_URL } from '../../dia-backend/secret'; import { NetworkAppOrder } from '../../dia-backend/store/dia-backend-store.service'; @@ -25,7 +25,7 @@ export class OrderHistoryService { constructor( private readonly httpClient: HttpClient, - private readonly webCryptoApiSignatureProvider: WebCryptoApiSignatureProvider, + private readonly capAppWebCryptoApiSignatureProvider: CaptureAppWebCryptoApiSignatureProvider, private readonly proofRepository: ProofRepository, private readonly sanitizer: DomSanitizer, private readonly diaBackendTransactionRepository: DiaBackendTransactionRepository, @@ -50,7 +50,7 @@ export class OrderHistoryService { createOrderHistory$(networkAppOrder: NetworkAppOrder, cid: string) { return defer(() => - this.webCryptoApiSignatureProvider.publicKey$.pipe( + this.capAppWebCryptoApiSignatureProvider.publicKey$.pipe( concatMap(publicKey => this.httpClient.post( `${BUBBLE_DB_URL}/api/1.1/obj/order`, @@ -77,7 +77,7 @@ export class OrderHistoryService { */ getOrdersHistory$() { return defer(() => - this.webCryptoApiSignatureProvider.publicKey$.pipe( + this.capAppWebCryptoApiSignatureProvider.publicKey$.pipe( concatMap(publicKey => this.httpClient .get>( diff --git a/src/app/shared/camera/camera.service.ts b/src/app/shared/camera/camera.service.ts index 42ed05359..f14b7e414 100644 --- a/src/app/shared/camera/camera.service.ts +++ b/src/app/shared/camera/camera.service.ts @@ -76,6 +76,7 @@ export class CameraService { resolve({ base64, mimeType: file.type as MimeType, + source: CameraSource.Camera, }) ); }; @@ -99,6 +100,7 @@ function cameraPhotoToPhoto(cameraPhoto: CameraPhoto): Media { mimeType: fromExtension(cameraPhoto.format), // eslint-disable-next-line @typescript-eslint/no-non-null-assertion base64: cameraPhoto.base64String!, + source: CameraSource.Camera, }; } diff --git a/src/app/shared/capture/capture.service.ts b/src/app/shared/capture/capture.service.ts index bb7287dad..b5cc22c7f 100644 --- a/src/app/shared/capture/capture.service.ts +++ b/src/app/shared/capture/capture.service.ts @@ -1,4 +1,5 @@ import { Injectable } from '@angular/core'; +import { CameraSource } from '@capacitor/camera'; import { BehaviorSubject } from 'rxjs'; import { MimeType } from '../../utils/mime-type'; import { CollectorService } from '../collector/collector.service'; @@ -22,10 +23,10 @@ export class CaptureService { private readonly collectorService: CollectorService ) {} - async capture(source: Media) { + async capture(media: Media) { const proof = await Proof.from( this.mediaStore, - { [source.base64]: { mimeType: source.mimeType } }, + { [media.base64]: { mimeType: media.mimeType } }, { timestamp: Date.now(), providers: {} }, {} ); @@ -37,7 +38,8 @@ export class CaptureService { ); const collected = await this.collectorService.run( await proof.getAssets(), - proof.timestamp + proof.timestamp, + media.source ); // eslint-disable-next-line rxjs/no-subject-value const newCollectingOldProofHashes = this._collectingOldProofHashes$.value; @@ -54,4 +56,5 @@ export class CaptureService { export interface Media { readonly mimeType: MimeType; readonly base64: string; + readonly source: CameraSource; } diff --git a/src/app/shared/collector/collector.service.spec.ts b/src/app/shared/collector/collector.service.spec.ts index d60452f16..673faea3b 100644 --- a/src/app/shared/collector/collector.service.spec.ts +++ b/src/app/shared/collector/collector.service.spec.ts @@ -1,5 +1,6 @@ /* eslint-disable class-methods-use-this */ import { TestBed } from '@angular/core/testing'; +import { CameraSource } from '@capacitor/camera'; import { MimeType } from '../../utils/mime-type'; import { AssetMeta, @@ -27,7 +28,7 @@ describe('CollectorService', () => { it('should be created', () => expect(service).toBeTruthy()); it('should get the stored proof after run', async () => { - const proof = await service.run(ASSETS, Date.now()); + const proof = await service.run(ASSETS, Date.now(), CameraSource.Camera); expect(await proof.getAssets()).toEqual(ASSETS); }); @@ -35,7 +36,7 @@ describe('CollectorService', () => { service.addFactsProvider(mockFactsProvider); service.removeFactsProvider(mockFactsProvider); - const proof = await service.run(ASSETS, Date.now()); + const proof = await service.run(ASSETS, Date.now(), CameraSource.Camera); expect(proof.truth.providers).toEqual({}); }); @@ -44,20 +45,20 @@ describe('CollectorService', () => { service.addSignatureProvider(mockSignatureProvider); service.removeSignatureProvider(mockSignatureProvider); - const proof = await service.run(ASSETS, Date.now()); + const proof = await service.run(ASSETS, Date.now(), CameraSource.Camera); expect(proof.signatures).toEqual({}); }); it('should get the stored proof with provided facts', async () => { service.addFactsProvider(mockFactsProvider); - const proof = await service.run(ASSETS, Date.now()); + const proof = await service.run(ASSETS, Date.now(), CameraSource.Camera); expect(proof.truth.providers).toEqual({ [mockFactsProvider.id]: FACTS }); }); it('should get the stored proof with provided signature', async () => { service.addSignatureProvider(mockSignatureProvider); - const proof = await service.run(ASSETS, Date.now()); + const proof = await service.run(ASSETS, Date.now(), CameraSource.Camera); expect(proof.signatures).toEqual({ [mockSignatureProvider.id]: SIGNATURE }); }); }); @@ -104,6 +105,9 @@ const SIGNATURE: Signature = { }; class MockSignatureProvider implements SignatureProvider { readonly id = 'MockSignatureProvider'; + idFor(_source: any): string { + return this.id; + } // eslint-disable-next-line @typescript-eslint/require-await async provide(_: string) { return SIGNATURE; diff --git a/src/app/shared/collector/collector.service.ts b/src/app/shared/collector/collector.service.ts index d46875117..e8f8754f8 100644 --- a/src/app/shared/collector/collector.service.ts +++ b/src/app/shared/collector/collector.service.ts @@ -1,4 +1,5 @@ import { Injectable } from '@angular/core'; +import { CameraSource } from '@capacitor/camera'; import { MediaStore } from '../media/media-store/media-store.service'; import { Assets, @@ -9,6 +10,7 @@ import { Truth, } from '../repositories/proof/proof'; import { FactsProvider } from './facts/facts-provider'; +import { CaptureAppWebCryptoApiSignatureProvider } from './signature/capture-app-web-crypto-api-signature-provider/capture-app-web-crypto-api-signature-provider.service'; import { SignatureProvider } from './signature/signature-provider'; @Injectable({ @@ -20,17 +22,19 @@ export class CollectorService { constructor(private readonly mediaStore: MediaStore) {} - async run(assets: Assets, capturedTimestamp: number) { + async run(assets: Assets, capturedTimestamp: number, source: CameraSource) { const truth = await this.collectTruth(assets, capturedTimestamp); const proof = await Proof.from(this.mediaStore, assets, truth); - await this.generateSignature(proof); + await this.generateSignature(proof, source); proof.isCollected = true; return proof; } - async generateSignature(proof: Proof) { - const signedMessage = await proof.generateSignedMessage(); - const signatures = await this.signMessage(signedMessage); + async generateSignature(proof: Proof, source: CameraSource) { + const recorder = + CaptureAppWebCryptoApiSignatureProvider.recorderFor(source); + const signedMessage = await proof.generateSignedMessage(recorder); + const signatures = await this.signMessage(signedMessage, source); proof.setSignatures(signatures); return proof; } @@ -52,13 +56,16 @@ export class CollectorService { }; } - private async signMessage(message: SignedMessage): Promise { + private async signMessage( + message: SignedMessage, + source: CameraSource + ): Promise { const serializedSortedSignedMessage = getSerializedSortedSignedMessage(message); return Object.fromEntries( await Promise.all( [...this.signatureProviders].map(async provider => [ - provider.id, + provider.idFor(source), await provider.provide(serializedSortedSignedMessage), ]) ) diff --git a/src/app/shared/collector/signature/web-crypto-api-signature-provider/web-crypto-api-signature-provider.service.spec.ts b/src/app/shared/collector/signature/capture-app-web-crypto-api-signature-provider/capture-app-web-crypto-api-signature-provider.service.spec.ts similarity index 82% rename from src/app/shared/collector/signature/web-crypto-api-signature-provider/web-crypto-api-signature-provider.service.spec.ts rename to src/app/shared/collector/signature/capture-app-web-crypto-api-signature-provider/capture-app-web-crypto-api-signature-provider.service.spec.ts index 9ca199875..0c1effc84 100644 --- a/src/app/shared/collector/signature/web-crypto-api-signature-provider/web-crypto-api-signature-provider.service.spec.ts +++ b/src/app/shared/collector/signature/capture-app-web-crypto-api-signature-provider/capture-app-web-crypto-api-signature-provider.service.spec.ts @@ -2,18 +2,22 @@ import { TestBed } from '@angular/core/testing'; import { defer } from 'rxjs'; import { concatMapTo } from 'rxjs/operators'; import { sortObjectDeeplyByKey } from '../../../../utils/immutable/immutable'; -import { isSignature, SignedMessage } from '../../../repositories/proof/proof'; +import { + isSignature, + RecorderType, + SignedMessage, +} from '../../../repositories/proof/proof'; import { SharedTestingModule } from '../../../shared-testing.module'; -import { WebCryptoApiSignatureProvider } from './web-crypto-api-signature-provider.service'; +import { CaptureAppWebCryptoApiSignatureProvider } from './capture-app-web-crypto-api-signature-provider.service'; -describe('WebCryptoApiSignatureProvider', () => { - let provider: WebCryptoApiSignatureProvider; +describe('CaptureAppWebCryptoApiSignatureProvider', () => { + let provider: CaptureAppWebCryptoApiSignatureProvider; beforeEach(() => { TestBed.configureTestingModule({ imports: [SharedTestingModule], }); - provider = TestBed.inject(WebCryptoApiSignatureProvider); + provider = TestBed.inject(CaptureAppWebCryptoApiSignatureProvider); }); it('should be created', () => expect(provider).toBeTruthy()); @@ -58,7 +62,7 @@ describe('WebCryptoApiSignatureProvider', () => { it('should provide signature', async () => { const signedMessage: SignedMessage = { spec_version: '', - recorder: '', + recorder: RecorderType.Capture, created_at: 0, proof_hash: '', asset_mime_type: '', diff --git a/src/app/shared/collector/signature/capture-app-web-crypto-api-signature-provider/capture-app-web-crypto-api-signature-provider.service.ts b/src/app/shared/collector/signature/capture-app-web-crypto-api-signature-provider/capture-app-web-crypto-api-signature-provider.service.ts new file mode 100644 index 000000000..34e18e27c --- /dev/null +++ b/src/app/shared/collector/signature/capture-app-web-crypto-api-signature-provider/capture-app-web-crypto-api-signature-provider.service.ts @@ -0,0 +1,124 @@ +import { Injectable } from '@angular/core'; +import { CameraSource } from '@capacitor/camera'; +import { + createEthAccount, + loadEthAccount, +} from '../../../../utils/crypto/crypto'; +import { PreferenceManager } from '../../../preference-manager/preference-manager.service'; +import { RecorderType, Signature } from '../../../repositories/proof/proof'; +import { SignatureProvider } from '../signature-provider'; + +@Injectable({ + providedIn: 'root', +}) +export class CaptureAppWebCryptoApiSignatureProvider + implements SignatureProvider +{ + readonly deprecatedProviderId = 'WebCryptoApiSignatureProvider'; + readonly id = 'CaptureAppWebCryptoApiSignatureProvider'; + + private readonly preferences = this.preferenceManager.getPreferences(this.id); + + readonly publicKey$ = this.preferences.getString$(PrefKeys.PUBLIC_KEY); + + readonly privateKey$ = this.preferences.getString$(PrefKeys.PRIVATE_KEY); + + constructor(private readonly preferenceManager: PreferenceManager) {} + + idFor(source: any): string { + switch (source) { + case CameraSource.Photos: + return 'UploaderWebCryptoApiSignatureProvider'; + case CameraSource.Camera: + return this.id; + default: + return this.id; + } + } + + /** + * Determines the appropriate recorder type based on the camera source. + * + * @param cameraSource - The CameraSource used for determining the recorder type + * @returns The RecorderType associated with the given camera source + */ + // eslint-disable-next-line @typescript-eslint/member-ordering + static recorderFor(source: CameraSource): RecorderType { + switch (source) { + case CameraSource.Photos: + return RecorderType.UploaderWebCryptoApiSignatureProvider; + case CameraSource.Camera: + return RecorderType.CaptureAppWebCryptoApiSignatureProvider; + default: + return RecorderType.Capture; + } + } + + async initialize() { + await this.copyKeysFromWebCryptoApiSignatureProviderIfAny(); + const originalPublicKey = await this.getPublicKey(); + const originalPrivateKey = await this.getPrivateKey(); + if ( + originalPublicKey.length === 0 || + originalPrivateKey.length === 0 || + !originalPublicKey.startsWith('0x') + ) { + const account = createEthAccount(); + await this.preferences.setString(PrefKeys.PUBLIC_KEY, account.address); + await this.preferences.setString( + PrefKeys.PRIVATE_KEY, + account.privateKey + ); + } + } + + async provide(serializedSortedSignedTargets: string): Promise { + await this.initialize(); + const account = loadEthAccount(await this.getPrivateKey()); + const sign = account.sign(serializedSortedSignedTargets); + const publicKey = await this.getPublicKey(); + return { signature: sign.signature, publicKey }; + } + + async getPublicKey() { + return this.preferences.getString(PrefKeys.PUBLIC_KEY); + } + + async getPrivateKey() { + return this.preferences.getString(PrefKeys.PRIVATE_KEY); + } + + async importKeys(publicKey: string, privateKey: string) { + await this.preferences.setString(PrefKeys.PUBLIC_KEY, publicKey); + await this.preferences.setString(PrefKeys.PRIVATE_KEY, privateKey); + } + + /** + * Will copy public, private key from WebCryptoApiSignatureProvider preferences + * to CaptureAppWebCryptoApiSignatureProvider preferences if there are any keys + */ + private async copyKeysFromWebCryptoApiSignatureProviderIfAny() { + const publicKey = await this.getWebCryptoApiSignatureProviderPublicKey(); + const privateKey = await this.getWebCryptoApiSignatureProviderPrivateKey(); + if (!!publicKey && !!privateKey) { + await this.importKeys(publicKey, privateKey); + } + } + + private async getWebCryptoApiSignatureProviderPublicKey() { + return this.preferenceManager + .getPreferences(this.deprecatedProviderId) + .getString(PrefKeys.PUBLIC_KEY); + } + + private async getWebCryptoApiSignatureProviderPrivateKey() { + return this.preferenceManager + .getPreferences(this.deprecatedProviderId) + .getString(PrefKeys.PRIVATE_KEY); + } +} + +const enum PrefKeys { + PUBLIC_KEY = 'PUBLIC_KEY', + PRIVATE_KEY = 'PRIVATE_KEY', +} diff --git a/src/app/shared/collector/signature/signature-provider.ts b/src/app/shared/collector/signature/signature-provider.ts index f563299c7..f0743255c 100644 --- a/src/app/shared/collector/signature/signature-provider.ts +++ b/src/app/shared/collector/signature/signature-provider.ts @@ -2,5 +2,6 @@ import { Signature } from '../../repositories/proof/proof'; export interface SignatureProvider { readonly id: string; + idFor(source: any): string; provide(serializedSortedSignedTargets: string): Promise; } diff --git a/src/app/shared/collector/signature/web-crypto-api-signature-provider/web-crypto-api-signature-provider.service.ts b/src/app/shared/collector/signature/web-crypto-api-signature-provider/web-crypto-api-signature-provider.service.ts deleted file mode 100644 index fb1647291..000000000 --- a/src/app/shared/collector/signature/web-crypto-api-signature-provider/web-crypto-api-signature-provider.service.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { Injectable } from '@angular/core'; -import { - createEthAccount, - loadEthAccount, -} from '../../../../utils/crypto/crypto'; -import { PreferenceManager } from '../../../preference-manager/preference-manager.service'; -import { Signature } from '../../../repositories/proof/proof'; -import { SignatureProvider } from '../signature-provider'; - -@Injectable({ - providedIn: 'root', -}) -export class WebCryptoApiSignatureProvider implements SignatureProvider { - readonly id = 'WebCryptoApiSignatureProvider'; - - private readonly preferences = this.preferenceManager.getPreferences(this.id); - - readonly publicKey$ = this.preferences.getString$(PrefKeys.PUBLIC_KEY); - - readonly privateKey$ = this.preferences.getString$(PrefKeys.PRIVATE_KEY); - - constructor(private readonly preferenceManager: PreferenceManager) {} - - async initialize() { - const originalPublicKey = await this.getPublicKey(); - const originalPrivateKey = await this.getPrivateKey(); - if ( - originalPublicKey.length === 0 || - originalPrivateKey.length === 0 || - !originalPublicKey.startsWith('0x') - ) { - const account = createEthAccount(); - await this.preferences.setString(PrefKeys.PUBLIC_KEY, account.address); - await this.preferences.setString( - PrefKeys.PRIVATE_KEY, - account.privateKey - ); - } - } - - async provide(serializedSortedSignedTargets: string): Promise { - await this.initialize(); - const account = loadEthAccount(await this.getPrivateKey()); - const sign = account.sign(serializedSortedSignedTargets); - const publicKey = await this.getPublicKey(); - return { signature: sign.signature, publicKey }; - } - - async getPublicKey() { - return this.preferences.getString(PrefKeys.PUBLIC_KEY); - } - - async getPrivateKey() { - return this.preferences.getString(PrefKeys.PRIVATE_KEY); - } - - async importKeys(publicKey: string, privateKey: string) { - await this.preferences.setString(PrefKeys.PUBLIC_KEY, publicKey); - await this.preferences.setString(PrefKeys.PRIVATE_KEY, privateKey); - } -} - -const enum PrefKeys { - PUBLIC_KEY = 'PUBLIC_KEY', - PRIVATE_KEY = 'PRIVATE_KEY', -} diff --git a/src/app/shared/migration/service/migration.service.ts b/src/app/shared/migration/service/migration.service.ts index 7d3a98df5..7095f5b80 100644 --- a/src/app/shared/migration/service/migration.service.ts +++ b/src/app/shared/migration/service/migration.service.ts @@ -1,6 +1,7 @@ import { HttpErrorResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; +import { CameraSource } from '@capacitor/camera'; import { defer, forkJoin, iif } from 'rxjs'; import { catchError, @@ -12,7 +13,7 @@ import { } from 'rxjs/operators'; import { VOID$ } from '../../../utils/rx-operators/rx-operators'; import { CollectorService } from '../../collector/collector.service'; -import { WebCryptoApiSignatureProvider } from '../../collector/signature/web-crypto-api-signature-provider/web-crypto-api-signature-provider.service'; +import { CaptureAppWebCryptoApiSignatureProvider } from '../../collector/signature/capture-app-web-crypto-api-signature-provider/capture-app-web-crypto-api-signature-provider.service'; import { DiaBackendAsset, DiaBackendAssetRepository, @@ -46,7 +47,7 @@ export class MigrationService { private readonly preferenceManager: PreferenceManager, private readonly onboardingService: OnboardingService, private readonly versionService: VersionService, - private readonly webCryptoApiSignatureProvider: WebCryptoApiSignatureProvider + private readonly capAppWebCryptoApiSignatureProvider: CaptureAppWebCryptoApiSignatureProvider ) {} migrate$(skip?: boolean) { @@ -161,7 +162,7 @@ export class MigrationService { err.status === HttpErrorCode.NOT_FOUND ) { return defer(() => - this.webCryptoApiSignatureProvider.getPrivateKey() + this.capAppWebCryptoApiSignatureProvider.getPrivateKey() ).pipe( concatMap(privateKey => this.diaBackendWalletService.setIntegrityWallet$(privateKey) @@ -171,7 +172,7 @@ export class MigrationService { throw err; }), concatMap(assetWallet => - this.webCryptoApiSignatureProvider.importKeys( + this.capAppWebCryptoApiSignatureProvider.importKeys( assetWallet.address, assetWallet.private_key ) @@ -185,7 +186,9 @@ export class MigrationService { map(proofs => proofs.filter(proof => !proof.signatureVersion)), concatMap(proofs => forkJoin( - proofs.map(proof => this.collectorService.generateSignature(proof)) + proofs.map(proof => + this.collectorService.generateSignature(proof, CameraSource.Camera) + ) ).pipe(defaultIfEmpty(proofs)) ), concatMap(proofs => diff --git a/src/app/shared/repositories/proof/proof.ts b/src/app/shared/repositories/proof/proof.ts index 705bcb03e..d4dd39ec4 100644 --- a/src/app/shared/repositories/proof/proof.ts +++ b/src/app/shared/repositories/proof/proof.ts @@ -11,7 +11,11 @@ import { OnWriteExistStrategy, } from '../../media/media-store/media-store.service'; -const RECORDER = 'Capture'; +export enum RecorderType { + Capture = 'Capture', + CaptureAppWebCryptoApiSignatureProvider = 'CaptureAppWebCryptoApiSignatureProvider', + UploaderWebCryptoApiSignatureProvider = 'UploaderWebCryptoApiSignatureProvider', +} const SIGNATURE_VERSION = '2.0.0'; export class Proof { @@ -200,10 +204,21 @@ export class Proof { return Object.fromEntries(factEntries) as Facts; } - async generateSignedMessage() { + /** + * Generates a signed message with the provided recorder type. + * + * @param recorderType - The type of recorder used for signing the message + * (default is RecorderType.CAPTURE). Related discussion comments: + * - https://github.com/numbersprotocol/capture-lite/issues/779#issuecomment-880330292 + * - https://app.asana.com/0/0/1204012493522134/1204289040001270/f + * @returns A promise that resolves to the generated signed message + */ + async generateSignedMessage( + recorder: RecorderType = RecorderType.Capture + ): Promise { const signedMessage: SignedMessage = { spec_version: SIGNATURE_VERSION, - recorder: RECORDER, + recorder: recorder, created_at: this.truth.timestamp, location_latitude: this.geolocationLatitude, location_longitude: this.geolocationLongitude, @@ -387,7 +402,7 @@ export interface IndexedProofView extends Tuple { */ export interface SignedMessage { spec_version: string; - recorder: string; + recorder: RecorderType; created_at: number; location_latitude?: number; location_longitude?: number;