Repository navigation
Develop #153
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
Merged
Develop #153
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
5c4199e
RR-T48 Expo 57 Fixes, Onboarding fixes, Records, Ops and Checklist Fixes
ucswift 47f69fa
RR-T49 Passkey support
ucswift 85b17df
RR-T49 PR #153 fixes
ucswift f0a435c
RR-T48 Allowing Diff Map Styles
ucswift 633eb79
RR-T48 Map style fix
ucswift File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| // Jest stand-in for expo-crypto, whose native AES classes cannot load under Jest. Randomness and digests use Node's | ||
| // crypto so PKCE values are real; tests that need fixed values still mock expo-crypto themselves, which takes precedence. | ||
| import { createHash, randomBytes, randomUUID as nodeRandomUUID } from 'crypto'; | ||
|
|
||
| export const CryptoDigestAlgorithm = { SHA1: 'SHA-1', SHA256: 'SHA-256', SHA384: 'SHA-384', SHA512: 'SHA-512', MD5: 'MD5' } as const; | ||
| export const CryptoEncoding = { HEX: 'hex', BASE64: 'base64' } as const; | ||
|
|
||
| const nodeAlgorithm = (algorithm: string) => algorithm.replace('-', '').toLowerCase(); | ||
|
|
||
| export const getRandomBytes = (byteCount: number): Uint8Array => new Uint8Array(randomBytes(byteCount)); | ||
| export const getRandomBytesAsync = async (byteCount: number): Promise<Uint8Array> => getRandomBytes(byteCount); | ||
| export const randomUUID = (): string => nodeRandomUUID(); | ||
| export const digestStringAsync = async (algorithm: string, data: string, options?: { encoding?: string }): Promise<string> => | ||
| createHash(nodeAlgorithm(algorithm)) | ||
| .update(data) | ||
| .digest(options?.encoding === 'base64' ? 'base64' : 'hex'); | ||
| export const digest = async (algorithm: string, data: ArrayBuffer | Uint8Array): Promise<ArrayBuffer> => { | ||
| // A copy into a fresh ArrayBuffer: Node's pooled Buffer may sit on a shared backing store. | ||
| return new Uint8Array(createHash(nodeAlgorithm(algorithm)).update(Buffer.from(data as ArrayBuffer)).digest()).buffer; | ||
| }; | ||
| export const aesEncryptAsync = jest.fn(); | ||
| export const aesDecryptAsync = jest.fn(); | ||
| export class AESEncryptionKey {} | ||
| export class AESSealedData {} | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| // Jest stand-in for react-native-passkey (a native module). Tests set the results they need on these mocks. | ||
| export const Passkey = { | ||
| isSupported: jest.fn(() => true), | ||
| get: jest.fn(), | ||
| create: jest.fn(), | ||
| getPlatformKey: jest.fn(), | ||
| createPlatformKey: jest.fn(), | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,171 @@ | ||
| import type { PasskeyCeremonyData, PasskeyData, SsoBeginData, SsoRedeemData } from '@/lib/mfa/types'; | ||
|
|
||
| import { api } from '../common/client'; | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // The signed-in side of sign-in methods (passkey plan sections 6.5, 7.6 and 7.9): the account methods view, passkeys for | ||
| // this app, Responder approval as the approver, and the fresh verification the server asks for before any change. | ||
| // Bodies are PascalCase; results carry their payload in `Data`. | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| const data = async <T>(request: Promise<{ data: { Data: T } }>): Promise<T> => (await request).data.Data; | ||
|
|
||
| // ---- Approving another app's sign-in (Responder is the approver) ----------------------------------------------------- | ||
|
|
||
| export interface PendingApprovalData { | ||
| ApprovalRequestId: string; | ||
| RequestingApp: string | null; | ||
| InstallationLabel: string | null; | ||
| Shared: boolean; | ||
| Department: string | null; | ||
| Purpose: 'login' | 'step_up' | 'adp' | 'unlock' | 'department_entry'; | ||
| Operation: string | null; | ||
| OriginRegion: string | null; | ||
| CreatedAt: string | null; | ||
| ExpiresAt: string | null; | ||
| AttemptsRemaining: number; | ||
| } | ||
|
|
||
| export const getPendingApproval = () => data<PendingApprovalData | null>(api.get('/MfaApproval/Pending')); | ||
|
|
||
| export const getApprovalOptions = (approvalRequestId: string) => data<PasskeyCeremonyData>(api.post('/MfaApproval/Options', { ApprovalRequestId: approvalRequestId })); | ||
|
|
||
| export const approveRequest = (approvalRequestId: string, matchNumber: string, requestId: string, credential: Record<string, unknown>) => | ||
| data<{ State: string }>(api.post('/MfaApproval/Approve', { ApprovalRequestId: approvalRequestId, MatchNumber: matchNumber, RequestId: requestId, Credential: credential })); | ||
|
|
||
| export const denyRequest = (approvalRequestId: string, reason: 'declined' | 'not_me') => data<{ State: string }>(api.post('/MfaApproval/Deny', { ApprovalRequestId: approvalRequestId, Reason: reason })); | ||
|
|
||
| // ---- Account methods view (plan section 6.5) -------------------------------------------------------------------------- | ||
|
|
||
| export interface TotpMethodData { | ||
| Enrolled: boolean; | ||
| EnrolledOn: string | null; | ||
| LastUsedOn: string | null; | ||
| RecoveryCodesRemaining: number; | ||
| RecoveryCodeWarning: boolean; | ||
| SetUpOnSharedInstallation: boolean; | ||
| CanTurnOff: boolean; | ||
| } | ||
|
|
||
| export interface PasskeyGroupData { | ||
| Client: string; | ||
| RegistrationAvailable: boolean; | ||
| Passkeys: PasskeyData[]; | ||
| } | ||
|
|
||
| export interface ApprovalInstallationData { | ||
| InstallationId: string; | ||
| Label: string | null; | ||
| Platform: string | null; | ||
| IsCurrent: boolean; | ||
| ApprovalsOn: boolean; | ||
| StoppedOn: string | null; | ||
| LastDecisionOn: string | null; | ||
| LastDecision: 'approved' | 'denied' | null; | ||
| } | ||
|
|
||
| export interface LinkedIdentityData { | ||
| DepartmentId: number; | ||
| DepartmentName: string | null; | ||
| ProviderType: string | null; | ||
| LinkedOn: string | null; | ||
| AcceptsProviderStepUp: boolean; | ||
| LastProviderStepUpOn: string | null; | ||
| } | ||
|
|
||
| export interface MfaActivityData { | ||
| ActivityId: string; | ||
| OccurredOn: string; | ||
| Method: string; | ||
| Purpose: string; | ||
| Successful: boolean; | ||
| Client: string | null; | ||
| Installation: string | null; | ||
| SharedInstallation: boolean; | ||
| IsCurrentSession: boolean; | ||
| ReportedOn: string | null; | ||
| } | ||
|
|
||
| export interface AccountMethodsData { | ||
| CurrentClient: string | null; | ||
| Totp: TotpMethodData; | ||
| PasskeyGroups: PasskeyGroupData[]; | ||
| ApprovalInstallations: ApprovalInstallationData[]; | ||
| LinkedIdentities: LinkedIdentityData[]; | ||
| RecentActivity: MfaActivityData[]; | ||
| } | ||
|
|
||
| export const getAccountMethods = () => data<AccountMethodsData>(api.get('/AccountSecurity/Methods')); | ||
|
|
||
| /** "This wasn't me": ends the session a verification served and sends a notice. */ | ||
| export const reportActivity = (activityId: string) => data<{ SessionEnded: boolean; NextSteps: string[] }>(api.post('/AccountSecurity/ReportActivity', { ActivityId: activityId })); | ||
|
|
||
| // ---- Fresh proof before a change (plan sections 6.2 and 7.6 row 14) ---------------------------------------------------- | ||
|
|
||
| export const reauthenticateWithPassword = (password: string) => data<{ VerifiedAt: string }>(api.post('/AccountSecurity/Reauthenticate', { Password: password })); | ||
|
|
||
| export interface SsoReauthSecrets { | ||
| returnTarget: string; | ||
| state: string; | ||
| codeChallenge: string; | ||
| platform: string; | ||
| } | ||
|
|
||
| /** For a member whose department signs in through its identity provider: reauthenticate there instead of with a password. */ | ||
| export const beginSsoReauthentication = (secrets: SsoReauthSecrets) => | ||
| data<SsoBeginData>( | ||
| api.post('/Sso/Begin', { | ||
| Purpose: 'reauth', | ||
| ReturnTarget: secrets.returnTarget, | ||
| State: secrets.state, | ||
| CodeChallenge: secrets.codeChallenge, | ||
| CodeChallengeMethod: 'S256', | ||
| Platform: secrets.platform, | ||
| }) | ||
| ); | ||
|
|
||
| export const redeemSsoReauthentication = (ssoTransactionId: string, ssoCode: string, codeVerifier: string) => | ||
| data<SsoRedeemData>(api.post('/Sso/Redeem', { SsoTransactionId: ssoTransactionId, SsoCode: ssoCode, CodeVerifier: codeVerifier })); | ||
|
|
||
| export interface StepUpOptionsData { | ||
| Methods: string[]; | ||
| Preferred: string | null; | ||
| EnrollmentRequired: boolean; | ||
| WindowMinutes: number; | ||
| Passkey: PasskeyCeremonyData | null; | ||
| } | ||
|
|
||
| export const getStepUpOptions = (operation: string) => data<StepUpOptionsData>(api.get('/Mfa/StepUpOptions', { params: { operation } })); | ||
|
|
||
| export interface VerifyStepUpInput { | ||
| Operation: string; | ||
| Method: 'totp' | 'passkey'; | ||
| Code?: string; | ||
| RequestId?: string; | ||
| Credential?: Record<string, unknown>; | ||
| } | ||
|
|
||
| export const verifyStepUp = (input: VerifyStepUpInput) => data<{ VerifiedAt: string; ExpiresAt: string }>(api.post('/Mfa/VerifyStepUp', input as unknown as Record<string, unknown>)); | ||
|
|
||
| // ---- Passkeys for this app (plan section 6.1) -------------------------------------------------------------------------- | ||
|
|
||
| export const getPasskeyRegistrationOptions = () => data<PasskeyCeremonyData>(api.post('/Passkeys/RegistrationOptions', {})); | ||
|
|
||
| export const completePasskeyRegistration = (requestId: string, credential: Record<string, unknown>, displayName: string) => | ||
| data<PasskeyData>(api.post('/Passkeys/CompleteRegistration', { RequestId: requestId, Credential: credential, DisplayName: displayName })); | ||
|
|
||
| export interface PasskeyChangeData { | ||
| Revoked: number; | ||
| SessionsEnded: number; | ||
| CurrentSessionEnded: boolean; | ||
| } | ||
|
|
||
| export const renamePasskey = (passkeyId: string, displayName: string) => data<PasskeyChangeData>(api.post('/Passkeys/Rename', { PasskeyId: passkeyId, DisplayName: displayName })); | ||
|
|
||
| export const revokePasskey = (passkeyId: string) => data<PasskeyChangeData>(api.post('/Passkeys/Revoke', { PasskeyId: passkeyId })); | ||
|
|
||
| /** Responder passkeys only: whether this passkey may approve other apps' sign-ins. */ | ||
| export const setPasskeyApproval = (passkeyId: string, enabled: boolean) => data<PasskeyChangeData>(api.post('/Passkeys/SetApproval', { PasskeyId: passkeyId, Enabled: enabled })); | ||
|
|
||
| export const stopApprovalInstallations = (installationId: string | null) => | ||
| data<{ InstallationsStopped: number; PasskeysStopped: number }>(api.post('/MfaApproval/Installations/Disable', installationId ? { InstallationId: installationId, All: false } : { All: true })); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getRandomBytesAsyncinvokes the synchronousgetRandomBytesimplementation from async code, blocking the event loop and misrepresenting the API's asynchronous behavior. Use the awaitablerandomBytesAsyncAPI and return its result as aUint8Array.Kody rule violation: Use Awaitable Methods in Async Code
Prompt for LLM
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.