在iOS中使用SecRandomCopyBytes()生成一个随机的256位密钥

5
我一直使用UUIDString作为存储在iPad上的文件的加密密钥,但由第三方进行的应用程序安全审查建议如下。
随着应用程序的启动,会生成并存储一个全局数据库密钥在钥匙串中。在生成过程中,使用iOS提供的NSUUID类的UUIDString方法。该函数生成由字母A到F、数字和连字符组成的随机字符串,并且不必要地限制了密钥空间,导致熵降低。 由于密钥仅由应用程序逻辑使用,无需被个人阅读、理解或处理,因此无需将密钥空间限制为可读字符。因此,应使用通过SecRandomCopyBytes()生成的随机256位密钥作为主密钥。
现在我已经搜索了很多并尝试了一些代码实现,但没有找到确切的东西。 我尝试过的内容:
NSMutableData* data = [NSMutableData dataWithLength:32];
int result = SecRandomCopyBytes(kSecRandomDefault, 32, data.mutableBytes);
NSLog(@"Description %d",result);

我的理解是这应该给我一个整数,然后我应该将其转换为NSString并将其用作我的键,但我相当确定这不是此处所需的,而且上述方法总是将结果返回为0。我完全迷失了,在此感谢任何帮助。谢谢。

加密是对字节的操作,而不是字符,包括密钥。并非所有字节值都是有效的字符。随机字节数组很少有可能是有效的NSUTF8String或任何编码中的可打印字符串。如果您想要一个字符串,请将字节转换为Base64或十六进制。UUID通过在生成过程中结合已知的唯一元素(如时间和MAC地址)来交换时间和空间的全局唯一性与随机性之间的平衡。这会降低该值的随机性。随机字节数组没有此安全性降低。 - zaph
5个回答

7
SecRandomCopyBytes的结果应该总是为0,除非发生了一些错误(我无法想象为什么会发生),那么结果将为-1。你不需要将其转换为NSString。
你要获取的是被写入可变字节部分的随机字节,这就是你将用作“主密钥”的内容,而不是UUID字符串。
我会这样做:
uint8_t randomBytes[16];
int result = SecRandomCopyBytes(kSecRandomDefault, 16, randomBytes);
if(result == 0) {
    NSMutableString *uuidStringReplacement = [[NSMutableString alloc] initWithCapacity:16*2];
    for(NSInteger index = 0; index < 16; index++)
    {
        [uuidStringReplacement appendFormat: @"%02x", randomBytes[index]];
    }
    NSLog(@"uuidStringReplacement is %@", uuidStringReplacement);
} else {
    NSLog(@"SecRandomCopyBytes failed for some reason");
}

使用UUIDString对我来说已经足够安全了,但听起来你的第三方安全审计公司正在努力证明他们的费用是合理的。
编辑:由于Vlad的替代答案,我现在开始收到负面评价,并且我无法删除我的答案(因为它仍然有接受的勾号),这里是我的代码的另一个版本。我使用16个随机字节(在转换为十六进制时会加倍)。

非常感谢...我猜他们正在努力。 - Ankit Srivastava
1
这个永远不会给我任何字符串,因为我认为它只是随机字节,因此可能无法转换为NSUTF8String编码的字符串。你有别的想法吗?我该怎么使用它呢? - Ankit Srivastava
虽然我可以获取到使用其他编码方式生成的NSString,例如NSUTF16StringEncoding和NSASCIIStringEncoding。所以无论如何感谢您,我猜测使用ASCII编码就足够让审阅者满意了。 - Ankit Srivastava
请注意名称:SecRandomCopyBytes,而不是字符。随机字节数组很可能不是有效的 NSUTF8String 或任何编码中可打印的字符串。如果您想要一个字符串,请将字节转换为 Base64 或十六进制。 - zaph
谢谢你的负评,@Vlad... 你使用Base64编码的结果肯定更好(而且易于阅读),所以我会给你点赞。 - Michael Dautermann
显示剩余4条评论

4

生成的NSData无法保证UTF16字符。

此方法将生成32字节的UTF字符串,相当于256位。(优点是这是纯文本,并且可以在GET请求中发送。)

由于Base64哈希的长度为(3/4)×(输入字符串的长度),我们可以计算出生成32字节哈希所需的输入长度为24字节长。注意:如果不可被整除,则Base64可能会在末尾填充一个、两个或零个'='字符。

在OSX 10.9和iOS 7中,您可以使用:

-[NSData base64EncodedDataWithOptions:]

这个方法可以用来生成你的UUID:
+ (NSString*)generateSecureUUID {
    NSMutableData *data = [NSMutableData dataWithLength:24];

    int result = SecRandomCopyBytes(NULL, 24, data.mutableBytes);

    NSAssert(result == 0, @"Error generating random bytes: %d", result);

    NSString *base64EncodedData = [data base64EncodedStringWithOptions:0];

    return base64EncodedData;
}

1
我很好奇errno在哪里定义的? - xi.lin
@Vald,实际上errno可以使用。https://www.man7.org/linux/man-pages/man3/errno.3.html 我认为它应该是一个全局变量。 - xi.lin

2

UUID是一个16字节(128位)的唯一标识符,因此您在此处不使用256位密钥。 此外,正如@zaph指出的那样,UUID使用硬件标识符和其他输入来保证唯一性。 这些可预测的因素绝对不是加密安全的。

您不必使用UUID作为加密密钥,相反,我建议使用32字节的base 64或十六进制编码数据,这样您就会拥有256位的加密安全密钥:

/** Generates a 256 bits cryptographically secure key.
 * The output will be a 44 characters base 64 string (32 bytes data
 * before the base 64 encoding).
 * @return A base 64 encoded 256 bits secure key.
 */
+ (NSString*)generateSecureKey
{
    NSMutableData *data = [NSMutableData dataWithLength:32];
    int result = SecRandomCopyBytes(kSecRandomDefault, 32, data.mutableBytes);
    if (result != noErr) {
        return nil;
    }
    return [data base64EncodedStringWithOptions:kNilOptions];
}

针对生成类似UUID(安全)随机数的问题,以下是一种不错的方法,但请注意这些只能生成128位的密钥:

/** Generates a 128 bits cryptographically secure key, formatted as a UUID.
 * Keep that you won't have the same guarantee for uniqueness
 * as you have with regular UUIDs.
 * @return A cryptographically secure UUID.
 */
+ (NSString*)generateCryptoSecureUUID
{
    unsigned char bytes[16];
    int result = SecRandomCopyBytes(kSecRandomDefault, 16, bytes);
    if (result != noErr) {
        return nil;
    }
    return [[NSUUID alloc] initWithUUIDBytes:bytes].UUIDString;
}

加密技术很棒,但正确使用它确实很困难(很容易留下安全漏洞)。我强烈推荐您使用RNCryptor,它会引导您使用良好的加密标准,确保您不会不安全地重复使用相同的密钥,正确地从密码中衍生加密密钥等。


result != noErr 应该改成 == 吗? - Albert Renshaw
@AlbertRenshaw 不是。noErr 意味着“没有错误”,所以如果 result != noErr,就意味着出了问题。 - Bruno Muniz
@BrunoMuniz 哎呀!我一定是认为它的意思是“不,出错”了。 ;P - Albert Renshaw

0

我尝试了这段代码,用于长度为16和字节为16的情况:

uint8_t randomBytes[16];
NSMutableString *ivStr;
int result = SecRandomCopyBytes(kSecRandomDefault, 16, randomBytes);
if(result == 0) {
    ivStr = [[NSMutableString alloc] initWithCapacity:16];
    for(NSInteger index = 0; index < 8; index++)
    {
        [ivStr appendFormat: @"%02x", randomBytes[index]];
    }
        NSLog(@"uuidStringReplacement is %@", ivStr);
} else {
    NSLog(@"SecRandomCopyBytes failed for some reason");
}

成功


0

由于密钥通常需要使用UTF-8编码并且“可读” - 即没有UTF-8控制字符 - 我决定使用SecRandomCopyBytes生成的随机生成字节进行过滤,以便它只包含基本拉丁Unicode块中的字符。

/*!
 * @brief Generates NSData from a randomly generated byte array with a specific number of bits
 * @param numberOfBits the number of bits the generated data must have
 * @return the randomly generated NSData
 */
+ (NSData *)randomKeyDataGeneratorWithNumberBits:(int)numberOfBits {
    int numberOfBytes = numberOfBits/8;
    uint8_t randomBytes[numberOfBytes];
    int result = SecRandomCopyBytes(kSecRandomDefault, numberOfBytes, randomBytes);
    if(result == 0) {
        return [NSData dataWithBytes:randomBytes length:numberOfBytes];
    } else {
        return nil;
    }
}

/*!
 * @brief Generates UTF-8 NSData from a randomly generated byte array with a specific number of bits
 * @param numberOfBits the number of bits the generated data must have
 * @return the randomly generated NSData
 */
+ (NSData *)randomKeyUTF8DataGeneratorWithNumberBits:(int)numberOfBits {
    NSMutableData *result = [[NSMutableData alloc] init];
    int numberOfBytes = numberOfBits/8;
    while (result.length < numberOfBytes) {
        // Creates a random byte
        NSData *byte = [self randomKeyDataGeneratorWithNumberBits:8];
        int asciiValue = [[[NSString alloc] initWithData:byte encoding:NSUTF8StringEncoding] characterAtIndex:0];
        // Checks if the byte is UTF-8
        if (asciiValue > 32 && asciiValue < 127) {
            [result appendData:byte];
        }
    }
    return result;
}

如果你想让你的密钥更加“可读”,你可以尝试将其转换为Base64 URL安全格式。

/*!
 * @brief Encodes a String Base 64 with URL and Filename Safe Alphabet
 * @discussion Base64url Encoding  The URL- and filename-safe Base64 encoding described in RFC 4648 [RFC4648] (https://tools.ietf.org/html/rfc4648)
 * @discussion Section 5 (https://tools.ietf.org/html/rfc4648#section-5)
 * @param string the string to be enconded
 * @return the encoded string
 */
+ (NSString *)base64URLandFilenameSafeString:(NSString *)string {
    NSString *base64String = string;
    base64String = [base64String stringByReplacingOccurrencesOfString:@"/"
                                                           withString:@"_"];
    base64String = [base64String stringByReplacingOccurrencesOfString:@"+"
                                                           withString:@"-"];
    return base64String;
}

生成一个UTF-8 256位的密钥:
NSData *key = [self randomKeyUTF8DataGeneratorWithNumberBits:256];
NSString *UTF8String = [[NSString alloc] initWithBytes:[key bytes] length:data.length encoding:NSUTF8StringEncoding];
NSString *base64URLSafeString = [self base64URLandFilenameSafeString:UTF8String];

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接