| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent a76525a commit 9154f61
4 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2,6 +2,7 @@ | |||
| 2 | 2 | /// <reference path="../types-ios/src/lib/ios/objc-x86_64/objc!CFNetwork.d.ts" /> | |
| 3 | 3 | /// <reference path="../types-ios/src/lib/ios/objc-x86_64/objc!CoreText.d.ts" /> | |
| 4 | 4 | /// <reference path="../types-ios/src/lib/ios/objc-x86_64/objc!Darwin.d.ts" /> | |
| 5 | + /// <reference path="../types-ios/src/lib/ios/objc-x86_64/objc!Security.d.ts" /> | ||
| 5 | 6 | /// <reference path="../types-ios/src/lib/ios/objc-x86_64/objc!_DarwinFoundation1.d.ts" /> | |
| 6 | 7 | /// <reference path="../types-ios/src/lib/ios/objc-x86_64/objc!_DarwinFoundation2.d.ts" /> | |
| 7 | 8 | /// <reference path="../types-ios/src/lib/ios/objc-x86_64/objc!_DarwinFoundation3.d.ts" /> | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -53,6 +53,14 @@ global.NSData = { | |||
| 53 | 53 | }, | |
| 54 | 54 | }; | |
| 55 | 55 | global.NSMutableData = { ...global.NSData }; | |
| 56 | + // Security's CSPRNG entry point, which the crypto shim feeds a typed array directly. The stub | ||
| 57 | + // fills it so specs can prove the bytes reach the caller's own view, and records the call. | ||
| 58 | + global.errSecSuccess = 0; | ||
| 59 | + global.kSecRandomDefault = { native: 'kSecRandomDefault' }; | ||
| 60 | + global.SecRandomCopyBytes = (rnd: any, count: number, bytes: Uint8Array) => { | ||
| 61 | + bytes.fill(0xab); | ||
| 62 | + return 0; | ||
| 63 | + }; | ||
| 56 | 64 | global.NSCCrypto = { | |
| 57 | 65 | randomUUID() { | |
| 58 | 66 | return 'native-uuid'; | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -30,39 +30,82 @@ describe('Crypto.getRandomValues', () => { | |||
| 30 | 30 | }); | |
| 31 | 31 | ||
| 32 | 32 | describe('Crypto.getRandomValues (iOS)', () => { | |
| 33 | - // The bytes belong to V8's BackingStore. freeWhenDone:NO is what keeps Foundation from | ||
| 34 | - // freeing an allocation that V8's ArrayBufferSweeper also frees. | ||
| 35 | - it('wraps the buffer without donating ownership to Foundation', () => { | ||
| 33 | + /** Runs `fn` and returns the arguments SecRandomCopyBytes received. */ | ||
| 34 | + function captureSecRandom(fn: () => void) { | ||
| 35 | + const spy = vi.spyOn(globalThis as any, 'SecRandomCopyBytes'); | ||
| 36 | + | ||
| 37 | + try { | ||
| 38 | + fn(); | ||
| 39 | + expect(spy).toHaveBeenCalledTimes(1); | ||
| 40 | + | ||
| 41 | + return spy.mock.calls[0] as any[]; | ||
| 42 | + } finally { | ||
| 43 | + spy.mockRestore(); | ||
| 44 | + } | ||
| 45 | + } | ||
| 46 | + | ||
| 47 | + // The view itself goes to Security: the runtime resolves it to V8's backing store at the | ||
| 48 | + // view's byte offset, so the fill lands in the caller's array with nothing in between. | ||
| 49 | + it('hands a byte view straight to SecRandomCopyBytes and the bytes land in it', () => { | ||
| 36 | 50 | const bytes = new Uint8Array(16); | |
| 37 | - const data = captureNativeCall((globalThis as any).NSCCrypto, () => crypto.getRandomValues(bytes)); | ||
| 51 | + const [rnd, count, target] = captureSecRandom(() => crypto.getRandomValues(bytes)); | ||
| 38 | 52 | ||
| 39 | - expect(data.selector).toBe('dataWithBytesNoCopyLengthFreeWhenDone'); | ||
| 40 | - expect(data.args[0]).toBe(bytes); | ||
| 41 | - expect(data.args[1]).toBe(16); | ||
| 42 | - expect(data.args[2]).toBe(false); | ||
| 53 | + expect(rnd).toBe((globalThis as any).kSecRandomDefault); | ||
| 54 | + expect(count).toBe(16); | ||
| 55 | + expect(target).toBe(bytes); | ||
| 56 | + expect(Array.from(bytes)).toEqual(new Array(16).fill(0xab)); | ||
| 43 | 57 | }); | |
| 44 | 58 | ||
| 45 | - it('wraps only the view window of an offset byte view', () => { | ||
| 46 | - const view = new Uint8Array(new ArrayBuffer(32), 4, 10); | ||
| 47 | - const data = captureNativeCall((globalThis as any).NSCCrypto, () => crypto.getRandomValues(view)); | ||
| 48 | - | ||
| 49 | - expect(data.selector).toBe('dataWithBytesNoCopyLengthFreeWhenDone'); | ||
| 50 | - expect(data.args[0]).toBe(view); | ||
| 51 | - expect(data.args[1]).toBe(10); | ||
| 52 | - expect(data.args[2]).toBe(false); | ||
| 59 | + it('fills only the window of an offset byte view', () => { | ||
| 60 | + const buffer = new ArrayBuffer(32); | ||
| 61 | + const view = new Uint8Array(buffer, 4, 10); | ||
| 62 | + const [, count, target] = captureSecRandom(() => crypto.getRandomValues(view)); | ||
| 63 | + | ||
| 64 | + expect(count).toBe(10); | ||
| 65 | + expect(target).toBe(view); | ||
| 66 | + const all = new Uint8Array(buffer); | ||
| 67 | + expect(Array.from(all.subarray(0, 4))).toEqual([0, 0, 0, 0]); | ||
| 68 | + expect(Array.from(all.subarray(4, 14))).toEqual(new Array(10).fill(0xab)); | ||
| 69 | + expect(Array.from(all.subarray(14))).toEqual(new Array(18).fill(0)); | ||
| 53 | 70 | }); | |
| 54 | 71 | ||
| 55 | - it('wraps only the view window of a non-byte typed array', () => { | ||
| 72 | + it('reinterprets a non-byte typed array over its window only', () => { | ||
| 56 | 73 | const buffer = new ArrayBuffer(32); | |
| 57 | - const data = captureNativeCall((globalThis as any).NSCCrypto, () => crypto.getRandomValues(new Uint32Array(buffer, 8, 2))); | ||
| 58 | - const wrapped = data.args[0] as Uint8Array; | ||
| 59 | - | ||
| 60 | - expect(data.selector).toBe('dataWithBytesNoCopyLengthFreeWhenDone'); | ||
| 61 | - expect(wrapped.buffer).toBe(buffer); | ||
| 62 | - expect(wrapped.byteOffset).toBe(8); | ||
| 63 | - expect(wrapped.byteLength).toBe(8); | ||
| 64 | - expect(data.args[1]).toBe(8); | ||
| 65 | - expect(data.args[2]).toBe(false); | ||
| 74 | + const words = new Uint32Array(buffer, 8, 2); | ||
| 75 | + const [, count, target] = captureSecRandom(() => crypto.getRandomValues(words)); | ||
| 76 | + | ||
| 77 | + expect(count).toBe(8); | ||
| 78 | + expect(target).toBeInstanceOf(Uint8Array); | ||
| 79 | + expect(target.buffer).toBe(buffer); | ||
| 80 | + expect(target.byteOffset).toBe(8); | ||
| 81 | + expect(target.byteLength).toBe(8); | ||
| 82 | + expect(Array.from(words)).toEqual([0xabababab, 0xabababab]); | ||
| 83 | + }); | ||
| 84 | + | ||
| 85 | + it('throws when Security reports a failure', () => { | ||
| 86 | + const spy = vi.spyOn(globalThis as any, 'SecRandomCopyBytes').mockReturnValue(-50); | ||
| 87 | + | ||
| 88 | + try { | ||
| 89 | + expect(() => crypto.getRandomValues(new Uint8Array(8))).toThrow(/SecRandomCopyBytes failed \(-50\)/); | ||
| 90 | + } finally { | ||
| 91 | + spy.mockRestore(); | ||
| 92 | + } | ||
| 93 | + }); | ||
| 94 | + | ||
| 95 | + // NSMutableData copies bytes it does not own and frees ones it does, so no NSData wrapper | ||
| 96 | + // can ever alias V8's memory: the shim must not route through the framework at all. | ||
| 97 | + it('never wraps the bytes in NSData or calls the framework', () => { | ||
| 98 | + const crypto_ = vi.spyOn((globalThis as any).NSCCrypto, 'getRandomValues'); | ||
| 99 | + const mutable = vi.spyOn((globalThis as any).NSMutableData, 'dataWithBytesNoCopyLengthFreeWhenDone'); | ||
| 100 | + | ||
| 101 | + try { | ||
| 102 | + crypto.getRandomValues(new Uint8Array(8)); | ||
| 103 | + expect(crypto_).not.toHaveBeenCalled(); | ||
| 104 | + expect(mutable).not.toHaveBeenCalled(); | ||
| 105 | + } finally { | ||
| 106 | + crypto_.mockRestore(); | ||
| 107 | + mutable.mockRestore(); | ||
| 108 | + } | ||
| 66 | 109 | }); | |
| 67 | 110 | }); | |
| 68 | 111 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -26,11 +26,14 @@ export class Crypto { | |||
| 26 | 26 | (<any>org).nativescript.winter_tc.Crypto.getRandomValues(bytes); | |
| 27 | 27 | } | |
| 28 | 28 | if (__IOS__) { | |
| 29 | - // The pointer is V8-owned: freeWhenDone must stay NO, or Foundation and V8's | ||
| 30 | - // ArrayBufferSweeper both free the same allocation. | ||
| 31 | - const data = NSMutableData.dataWithBytesNoCopyLengthFreeWhenDone(bytes as never, bytes.byteLength, false); | ||
| 32 | - | ||
| 33 | - NSCCrypto.getRandomValues(data); | ||
| 29 | + // The view goes to Security directly: the runtime hands over V8's backing store at | ||
| 30 | + // the view's byte offset, so the bytes land in the caller's array. No NSData may | ||
| 31 | + // sit in between — NSMutableData copies bytes it does not own, so a no-copy wrapper | ||
| 32 | + // fills a private copy, and one that owns them frees V8's allocation. | ||
| 33 | + const status = SecRandomCopyBytes(kSecRandomDefault, bytes.byteLength, bytes); | ||
| 34 | + if (status !== errSecSuccess) { | ||
| 35 | + throw new Error(`getRandomValues: SecRandomCopyBytes failed (${status})`); | ||
| 36 | + } | ||
| 34 | 37 | } | |
| 35 | 38 | ||
| 36 | 39 | return typedArray; | |
| Back | FazBrowse Home | New Git URL |
0 commit comments