mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 05:43:41 +00:00
[PM-9035] desktop build logic to provide credentials to os on sync (#10181)
* feat: scaffold desktop_objc * feat: rename fido2 to autofill * feat: scaffold electron autofill * feat: auto call hello world on init * feat: scaffold call to basic objc function * feat: simple log that checks if autofill is enabled * feat: adding some availability guards * feat: scaffold services and allow calls from inspector * feat: create custom type for returning strings across rust/objc boundary * chore: clean up comments * feat: enable ARC * feat: add util function `c_string_to_nsstring` * chore: refactor and rename to `run_command` * feat: add try-catch around command execution * feat: properly implement command calling Add static typing. Add proper error handling. * feat: add autoreleasepool to avoid memory leaks * chore: change objc names to camelCase * fix: error returning * feat: extract some helper functions into utils class * feat: scaffold status command * feat: implement status command * feat: implement password credential mapping * wip: implement sync command This crashes because we are not properly handling the fact that `saveCredentialIdentities` uses callbacks, resulting in a race condition where we try to access a variable (result) that has already gotten dealloc'd. * feat: first version of callback * feat: make run_command async * feat: functioning callback returns * chore: refactor to make objc code easier to read and use * feat: refactor everything to use new callback return method * feat: re-implement status command with callback * fix: warning about CommandContext not being FFI-safe * feat: implement sync command using callbacks * feat: implement manual password credential sync * feat: add auto syncing * docs: add todo * feat: add support for passkeys * chore: move desktop autofill service to init service * feat: auto-add all .m files to builder * fix: native build on unix and windows * fix: unused compiler warnings * fix: napi type exports * feat: add corresponding dist command * feat: comment signing profile until we fix signing * fix: build breaking on non-macOS platforms * chore: cargo lock update * chore: revert accidental version change * feat: put sync behind feature flag * chore: put files in autofill folder * fix: obj-c code not recompiling on changes * feat: add `namespace` to commands * fix: linting complaining about flag * feat: add autofill as owner of their objc code * chore: make autofill owner of run_command in core crate * fix: re-add napi annotation * fix: remove dev bypass
This commit is contained in:
21
apps/desktop/desktop_native/objc/Cargo.toml
Normal file
21
apps/desktop/desktop_native/objc/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
edition = "2021"
|
||||
license = "GPL-3.0"
|
||||
name = "desktop_objc"
|
||||
version = "0.0.0"
|
||||
publish = false
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
[dependencies]
|
||||
anyhow = "=1.0.93"
|
||||
thiserror = "=1.0.69"
|
||||
tokio = "1.39.1"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
core-foundation = "=0.9.4"
|
||||
|
||||
[build-dependencies]
|
||||
cc = "1.0.104"
|
||||
glob = "0.3.1"
|
||||
22
apps/desktop/desktop_native/objc/build.rs
Normal file
22
apps/desktop/desktop_native/objc/build.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use glob::glob;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn main() {
|
||||
let mut builder = cc::Build::new();
|
||||
|
||||
// Auto compile all .m files in the src/native directory
|
||||
for entry in glob("src/native/**/*.m").expect("Failed to read glob pattern") {
|
||||
let path = entry.expect("Failed to read glob entry");
|
||||
builder.file(path.clone());
|
||||
println!("cargo::rerun-if-changed={}", path.display());
|
||||
}
|
||||
|
||||
builder
|
||||
.flag("-fobjc-arc") // Enable Auto Reference Counting (ARC)
|
||||
.compile("autofill");
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn main() {
|
||||
// Crate is only supported on macOS
|
||||
}
|
||||
124
apps/desktop/desktop_native/objc/src/lib.rs
Normal file
124
apps/desktop/desktop_native/objc/src/lib.rs
Normal file
@@ -0,0 +1,124 @@
|
||||
#![cfg(target_os = "macos")]
|
||||
|
||||
use std::{
|
||||
ffi::{c_char, CStr, CString},
|
||||
os::raw::c_void,
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
#[repr(C)]
|
||||
pub struct ObjCString {
|
||||
value: *const c_char,
|
||||
size: usize,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct CommandContext {
|
||||
tx: Option<tokio::sync::oneshot::Sender<String>>,
|
||||
}
|
||||
|
||||
impl CommandContext {
|
||||
pub fn new() -> (Self, tokio::sync::oneshot::Receiver<String>) {
|
||||
let (tx, rx) = tokio::sync::oneshot::channel::<String>();
|
||||
|
||||
(CommandContext { tx: Some(tx) }, rx)
|
||||
}
|
||||
|
||||
pub fn send(&mut self, value: String) -> Result<()> {
|
||||
let tx = self.tx.take().context(
|
||||
"Failed to take Sender from CommandContext. Has this context already returned once?",
|
||||
)?;
|
||||
|
||||
tx.send(value).map_err(|_| {
|
||||
anyhow::anyhow!("Failed to send ObjCString from CommandContext to Rust code")
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn as_ptr(&mut self) -> *mut c_void {
|
||||
self as *mut Self as *mut c_void
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<ObjCString> for String {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: ObjCString) -> Result<Self> {
|
||||
let c_str = unsafe { CStr::from_ptr(value.value) };
|
||||
let str = c_str
|
||||
.to_str()
|
||||
.context("Failed to convert ObjC output string to &str for use in Rust")?;
|
||||
|
||||
Ok(str.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ObjCString {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
objc::freeObjCString(self);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod objc {
|
||||
use std::os::raw::c_void;
|
||||
|
||||
use super::*;
|
||||
|
||||
extern "C" {
|
||||
pub fn runCommand(context: *mut c_void, value: *const c_char);
|
||||
pub fn freeObjCString(value: &ObjCString);
|
||||
}
|
||||
|
||||
/// This function is called from the ObjC code to return the output of the command
|
||||
#[no_mangle]
|
||||
pub extern "C" fn commandReturn(context: &mut CommandContext, value: ObjCString) -> bool {
|
||||
let value: String = match value.try_into() {
|
||||
Ok(value) => value,
|
||||
Err(e) => {
|
||||
println!(
|
||||
"Error: Failed to convert ObjCString to Rust string during commandReturn: {}",
|
||||
e
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
match context.send(value) {
|
||||
Ok(_) => 0,
|
||||
Err(e) => {
|
||||
println!(
|
||||
"Error: Failed to return ObjCString from ObjC code to Rust code: {}",
|
||||
e
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_command(input: String) -> Result<String> {
|
||||
// Convert input to type that can be passed to ObjC code
|
||||
let c_input = CString::new(input)
|
||||
.context("Failed to convert Rust input string to a CString for use in call to ObjC code")?;
|
||||
|
||||
let (mut context, rx) = CommandContext::new();
|
||||
|
||||
// Call ObjC code
|
||||
unsafe { objc::runCommand(context.as_ptr(), c_input.as_ptr()) };
|
||||
|
||||
// Convert output from ObjC code to Rust string
|
||||
let objc_output = rx.await?.try_into()?;
|
||||
|
||||
// Convert output from ObjC code to Rust string
|
||||
// let objc_output = output.try_into()?;
|
||||
|
||||
Ok(objc_output)
|
||||
}
|
||||
2
apps/desktop/desktop_native/objc/src/native/.clangd
Normal file
2
apps/desktop/desktop_native/objc/src/native/.clangd
Normal file
@@ -0,0 +1,2 @@
|
||||
CompileFlags:
|
||||
Add: [-fobjc-arc]
|
||||
@@ -0,0 +1,8 @@
|
||||
#ifndef STATUS_H
|
||||
#define STATUS_H
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
void status(void *context, NSDictionary *params);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,57 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <AuthenticationServices/ASCredentialIdentityStore.h>
|
||||
#import <AuthenticationServices/ASCredentialIdentityStoreState.h>
|
||||
#import "../../interop.h"
|
||||
#import "status.h"
|
||||
|
||||
void storeState(void (^callback)(ASCredentialIdentityStoreState*)) {
|
||||
if (@available(macos 11, *)) {
|
||||
ASCredentialIdentityStore *store = [ASCredentialIdentityStore sharedStore];
|
||||
[store getCredentialIdentityStoreStateWithCompletion:^(ASCredentialIdentityStoreState * _Nonnull state) {
|
||||
callback(state);
|
||||
}];
|
||||
} else {
|
||||
callback(nil);
|
||||
}
|
||||
}
|
||||
|
||||
BOOL fido2Supported() {
|
||||
if (@available(macos 14, *)) {
|
||||
return YES;
|
||||
} else {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
BOOL passwordSupported() {
|
||||
if (@available(macos 11, *)) {
|
||||
return YES;
|
||||
} else {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
void status(void* context, __attribute__((unused)) NSDictionary *params) {
|
||||
storeState(^(ASCredentialIdentityStoreState *state) {
|
||||
BOOL enabled = NO;
|
||||
BOOL supportsIncremental = NO;
|
||||
|
||||
if (state != nil) {
|
||||
enabled = state.isEnabled;
|
||||
supportsIncremental = state.supportsIncrementalUpdates;
|
||||
}
|
||||
|
||||
_return(context,
|
||||
_success(@{
|
||||
@"support": @{
|
||||
@"fido2": @(fido2Supported()),
|
||||
@"password": @(passwordSupported()),
|
||||
@"incrementalUpdates": @(supportsIncremental),
|
||||
},
|
||||
@"state": @{
|
||||
@"enabled": @(enabled),
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
#ifndef SYNC_H
|
||||
#define SYNC_H
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
void runSync(void *context, NSDictionary *params);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,59 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <AuthenticationServices/ASCredentialIdentityStore.h>
|
||||
#import <AuthenticationServices/ASCredentialIdentityStoreState.h>
|
||||
#import <AuthenticationServices/ASCredentialServiceIdentifier.h>
|
||||
#import <AuthenticationServices/ASPasswordCredentialIdentity.h>
|
||||
#import <AuthenticationServices/ASPasskeyCredentialIdentity.h>
|
||||
#import "../../utils.h"
|
||||
#import "../../interop.h"
|
||||
#import "sync.h"
|
||||
|
||||
// 'run' is added to the name because it clashes with internal macOS function
|
||||
void runSync(void* context, NSDictionary *params) {
|
||||
NSArray *credentials = params[@"credentials"];
|
||||
|
||||
// Map credentials to ASPasswordCredential objects
|
||||
NSMutableArray *mappedCredentials = [NSMutableArray arrayWithCapacity:credentials.count];
|
||||
for (NSDictionary *credential in credentials) {
|
||||
NSString *type = credential[@"type"];
|
||||
|
||||
if ([type isEqualToString:@"password"]) {
|
||||
NSString *cipherId = credential[@"cipherId"];
|
||||
NSString *uri = credential[@"uri"];
|
||||
NSString *username = credential[@"username"];
|
||||
|
||||
ASCredentialServiceIdentifier *serviceId = [[ASCredentialServiceIdentifier alloc]
|
||||
initWithIdentifier:uri type:ASCredentialServiceIdentifierTypeURL];
|
||||
ASPasswordCredentialIdentity *credential = [[ASPasswordCredentialIdentity alloc]
|
||||
initWithServiceIdentifier:serviceId user:username recordIdentifier:cipherId];
|
||||
|
||||
[mappedCredentials addObject:credential];
|
||||
}
|
||||
|
||||
if ([type isEqualToString:@"fido2"]) {
|
||||
NSString *cipherId = credential[@"cipherId"];
|
||||
NSString *rpId = credential[@"rpId"];
|
||||
NSString *userName = credential[@"userName"];
|
||||
NSData *credentialId = decodeBase64URL(credential[@"credentialId"]);
|
||||
NSData *userHandle = decodeBase64URL(credential[@"userHandle"]);
|
||||
|
||||
ASPasskeyCredentialIdentity *credential = [[ASPasskeyCredentialIdentity alloc]
|
||||
initWithRelyingPartyIdentifier:rpId
|
||||
userName:userName
|
||||
credentialID:credentialId
|
||||
userHandle:userHandle
|
||||
recordIdentifier:cipherId];
|
||||
|
||||
[mappedCredentials addObject:credential];
|
||||
}
|
||||
}
|
||||
|
||||
[ASCredentialIdentityStore.sharedStore replaceCredentialIdentityEntries:mappedCredentials
|
||||
completion:^(__attribute__((unused)) BOOL success, NSError * _Nullable error) {
|
||||
if (error) {
|
||||
return _return(context, _error_er(error));
|
||||
}
|
||||
|
||||
_return(context, _success(@{@"added": @([mappedCredentials count])}));
|
||||
}];
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
#ifndef RUN_AUTOFILL_COMMAND_H
|
||||
#define RUN_AUTOFILL_COMMAND_H
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
void runAutofillCommand(void* context, NSDictionary *input);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,20 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "commands/sync.h"
|
||||
#import "commands/status.h"
|
||||
#import "../interop.h"
|
||||
#import "../utils.h"
|
||||
#import "run_autofill_command.h"
|
||||
|
||||
void runAutofillCommand(void* context, NSDictionary *input) {
|
||||
NSString *command = input[@"command"];
|
||||
NSDictionary *params = input[@"params"];
|
||||
|
||||
if ([command isEqual:@"status"]) {
|
||||
return status(context, params);
|
||||
} else if ([command isEqual:@"sync"]) {
|
||||
return runSync(context, params);
|
||||
}
|
||||
|
||||
_return(context, _error([NSString stringWithFormat:@"Unknown command: %@", command]));
|
||||
}
|
||||
|
||||
47
apps/desktop/desktop_native/objc/src/native/interop.h
Normal file
47
apps/desktop/desktop_native/objc/src/native/interop.h
Normal file
@@ -0,0 +1,47 @@
|
||||
#ifndef INTEROP_H
|
||||
#define INTEROP_H
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
// Tips for developing Objective-C code:
|
||||
// - Use the `NSLog` function to log messages to the system log
|
||||
// - Example:
|
||||
// NSLog(@"An example log: %@", someVariable);
|
||||
// - Use the `@try` and `@catch` directives to catch exceptions
|
||||
|
||||
#if !__has_feature(objc_arc)
|
||||
// Auto Reference Counting makes memory management easier for Objective-C objects
|
||||
// Regular C objects still need to be managed manually
|
||||
#error ARC must be enabled!
|
||||
#endif
|
||||
|
||||
/// [Shared with Rust]
|
||||
/// Simple struct to hold a C-string and its length
|
||||
/// This is used to return strings created in Objective-C to Rust
|
||||
/// so that Rust can free the memory when it's done with the string
|
||||
struct ObjCString
|
||||
{
|
||||
char *value;
|
||||
size_t size;
|
||||
};
|
||||
|
||||
/// [Defined in Rust]
|
||||
/// External function callable from Objective-C to return a string to Rust
|
||||
extern bool commandReturn(void *context, struct ObjCString output);
|
||||
|
||||
/// [Callable from Rust]
|
||||
/// Frees the memory allocated for an ObjCString
|
||||
void freeObjCString(struct ObjCString *value);
|
||||
|
||||
// --- Helper functions to convert between Objective-C and Rust types ---
|
||||
|
||||
NSString *_success(NSDictionary *value);
|
||||
NSString *_error(NSString *error);
|
||||
NSString *_error_er(NSError *error);
|
||||
NSString *_error_ex(NSException *error);
|
||||
void _return(void *context, NSString *output);
|
||||
|
||||
struct ObjCString nsStringToObjCString(NSString *string);
|
||||
NSString *cStringToNSString(char *string);
|
||||
|
||||
#endif
|
||||
71
apps/desktop/desktop_native/objc/src/native/interop.m
Normal file
71
apps/desktop/desktop_native/objc/src/native/interop.m
Normal file
@@ -0,0 +1,71 @@
|
||||
#import "interop.h"
|
||||
#import "utils.h"
|
||||
|
||||
/// [Callable from Rust]
|
||||
/// Frees the memory allocated for an ObjCString
|
||||
void freeObjCString(struct ObjCString *value) {
|
||||
free(value->value);
|
||||
}
|
||||
|
||||
// --- Helper functions to convert between Objective-C and Rust types ---
|
||||
|
||||
NSString *_success(NSDictionary *value) {
|
||||
NSDictionary *wrapper = @{@"type": @"success", @"value": value};
|
||||
NSError *jsonError = nil;
|
||||
NSString *toReturn = serializeJson(wrapper, jsonError);
|
||||
|
||||
if (jsonError) {
|
||||
// Manually format message since there seems to be an issue with the JSON serialization
|
||||
return [NSString stringWithFormat:@"{\"type\": \"error\", \"error\": \"Error occurred while serializing error: %@\"}", jsonError];
|
||||
}
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
NSString *_error(NSString *error) {
|
||||
NSDictionary *errorDictionary = @{@"type": @"error", @"error": error};
|
||||
NSError *jsonError = nil;
|
||||
NSString *toReturn = serializeJson(errorDictionary, jsonError);
|
||||
|
||||
if (jsonError) {
|
||||
// Manually format message since there seems to be an issue with the JSON serialization
|
||||
return [NSString stringWithFormat:@"{\"type\": \"error\", \"error\": \"Error occurred while serializing error: %@\"}", jsonError];
|
||||
}
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
NSString *_error_er(NSError *error) {
|
||||
return _error([error localizedDescription]);
|
||||
}
|
||||
|
||||
NSString *_error_ex(NSException *error) {
|
||||
return _error([NSString stringWithFormat:@"%@ (%@): %@", error.name, error.reason, [error callStackSymbols]]);
|
||||
}
|
||||
|
||||
void _return(void* context, NSString *output) {
|
||||
if (!commandReturn(context, nsStringToObjCString(output))) {
|
||||
NSLog(@"Error: Failed to return command output");
|
||||
// NOTE: This will most likely crash the application
|
||||
@throw [NSException exceptionWithName:@"CommandReturnError" reason:@"Failed to return command output" userInfo:nil];
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts an NSString to an ObjCString struct
|
||||
struct ObjCString nsStringToObjCString(NSString* string) {
|
||||
size_t size = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
|
||||
char *value = malloc(size);
|
||||
[string getCString:value maxLength:size encoding:NSUTF8StringEncoding];
|
||||
|
||||
struct ObjCString objCString;
|
||||
objCString.value = value;
|
||||
objCString.size = size;
|
||||
|
||||
return objCString;
|
||||
}
|
||||
|
||||
/// Converts a C-string to an NSString
|
||||
NSString* cStringToNSString(char* string) {
|
||||
return [[NSString alloc] initWithUTF8String:string];
|
||||
}
|
||||
|
||||
39
apps/desktop/desktop_native/objc/src/native/run_command.m
Normal file
39
apps/desktop/desktop_native/objc/src/native/run_command.m
Normal file
@@ -0,0 +1,39 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "autofill/run_autofill_command.h"
|
||||
#import "interop.h"
|
||||
#import "utils.h"
|
||||
|
||||
void pickAndRunCommand(void* context, NSDictionary *input) {
|
||||
NSString *namespace = input[@"namespace"];
|
||||
|
||||
if ([namespace isEqual:@"autofill"]) {
|
||||
return runAutofillCommand(context, input);
|
||||
}
|
||||
|
||||
_return(context, _error([NSString stringWithFormat:@"Unknown namespace: %@", namespace]));
|
||||
}
|
||||
|
||||
/// [Callable from Rust]
|
||||
/// Runs a command with the given input JSON
|
||||
/// This function is called from Rust and is the entry point for running Objective-C code.
|
||||
/// It takes a JSON string as input, deserializes it, runs the command, and serializes the output.
|
||||
/// It also catches any exceptions that occur during the command execution.
|
||||
void runCommand(void *context, char* inputJson) {
|
||||
@autoreleasepool {
|
||||
@try {
|
||||
NSString *inputString = cStringToNSString(inputJson);
|
||||
|
||||
NSError *error = nil;
|
||||
NSDictionary *input = parseJson(inputString, error);
|
||||
if (error) {
|
||||
NSLog(@"Error occured while deserializing input params: %@", error);
|
||||
return _return(context, _error([NSString stringWithFormat:@"Error occured while deserializing input params: %@", error]));
|
||||
}
|
||||
|
||||
pickAndRunCommand(context, input);
|
||||
} @catch (NSException *e) {
|
||||
NSLog(@"Error occurred while running Objective-C command: %@", e);
|
||||
_return(context, _error([NSString stringWithFormat:@"Error occurred while running Objective-C command: %@", e]));
|
||||
}
|
||||
}
|
||||
}
|
||||
11
apps/desktop/desktop_native/objc/src/native/utils.h
Normal file
11
apps/desktop/desktop_native/objc/src/native/utils.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#ifndef UTILS_H
|
||||
#define UTILS_H
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NSDictionary *parseJson(NSString *jsonString, NSError *error);
|
||||
NSString *serializeJson(NSDictionary *dictionary, NSError *error);
|
||||
|
||||
NSData *decodeBase64URL(NSString *base64URLString);
|
||||
|
||||
#endif
|
||||
28
apps/desktop/desktop_native/objc/src/native/utils.m
Normal file
28
apps/desktop/desktop_native/objc/src/native/utils.m
Normal file
@@ -0,0 +1,28 @@
|
||||
#import "utils.h"
|
||||
|
||||
NSDictionary *parseJson(NSString *jsonString, NSError *error) {
|
||||
NSData *data = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
|
||||
if (error) {
|
||||
return nil;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
NSString *serializeJson(NSDictionary *dictionary, NSError *error) {
|
||||
NSData *data = [NSJSONSerialization dataWithJSONObject:dictionary options:0 error:&error];
|
||||
if (error) {
|
||||
return nil;
|
||||
}
|
||||
return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
||||
}
|
||||
|
||||
NSData *decodeBase64URL(NSString *base64URLString) {
|
||||
NSString *base64String = [base64URLString stringByReplacingOccurrencesOfString:@"-" withString:@"+"];
|
||||
base64String = [base64String stringByReplacingOccurrencesOfString:@"_" withString:@"/"];
|
||||
|
||||
NSData *nsdataFromBase64String = [[NSData alloc]
|
||||
initWithBase64EncodedString:base64String options:0];
|
||||
|
||||
return nsdataFromBase64String;
|
||||
}
|
||||
Reference in New Issue
Block a user